Back to Blog
Java

Java Static Fields: Lifecycle, Memory, and Thread Safety

java static fields: How Java static fields behave: initialization timing, class loading, memory visibility, thread safety, and when to avoid shared static state.

static-fieldsclass-loadingthread-safetymemory-modeljava-concurrency
Diagram showing a Java class with a static field shared across multiple instances and threads

Java static fields belong to the class itself rather than to any object created from that class. When you declare a field with the static modifier, the JVM allocates a single storage location for it when the class is loaded. Every instance of the class, and every direct reference to the class, sees the same value.

public class Counter { private static int total = 0; private int instanceCount = 0; public void increment() { total++; instanceCount++; } }

In this example, total is shared across all Counter objects, while instanceCount is separate for each object. If two Counter instances both call increment(), total becomes 2, but each instance has its own instanceCount of 1.

The practical difference is storage and identity. Instance fields are allocated per object and live as long as the object is reachable. Static fields are allocated once per class loader and live as long as the class remains loaded.

When and How Static Fields Are Initialized

Static fields are initialized during class initialization, which happens the first time the class is actively used. Active use includes creating an instance, invoking a static method, reading or writing a static field, or accessing a static member through reflection.

public class AppConfig { private static Properties config = loadConfig(); private static Properties loadConfig() { Properties props = new Properties(); props.setProperty("host", "localhost"); return props; } }

The loadConfig() call runs once, when AppConfig is first used. Subsequent references to AppConfig.config return the already-initialized value.

Initialization follows declaration order. If one static field depends on another, the dependency must appear first in the source:

public class Settings { private static Properties props = loadProperties(); private static String host = props.getProperty("host"); private static Properties loadProperties() { Properties loaded = new Properties(); // Populate from a configuration source return loaded; } }

Here props must be declared before host, because host reads from it during initialization. Reversing the order causes a NullPointerException at class initialization time.

Class Loading and the Lifecycle of Static State

A class is loaded once by a given class loader. The static fields exist for as long as that class loader keeps the class. In a simple application, that is effectively the lifetime of the JVM. In an application server that deploys and undeploys applications, each deployment typically uses its own class loader, so static state is discarded when the application is undeployed.

This has a practical consequence: static state is not guaranteed to survive across redeployments, and it is not shared between different class loaders. Two copies of the same class loaded by different class loaders have independent static fields.

Memory Visibility and Thread Safety

Because static fields are shared across all threads, they are subject to the Java memory model. A write to a static field in one thread is not guaranteed to be visible to another thread unless there is a happens-before relationship between the two actions.

public class Config { private static boolean initialized = false; public static void init() { // Set up resources initialized = true; } public static boolean isInitialized() { return initialized; } }

If one thread calls init() and another thread calls isInitialized(), the second thread may see initialized as false even after the first thread has completed. The fix depends on the access pattern:

  • volatile makes the write visible immediately and prevents reordering.
  • synchronized provides both visibility and atomicity.
  • AtomicBoolean or similar classes provide atomic updates.
public class Config { private static volatile boolean initialized = false; }

For compound operations like total++, volatile is not enough because the read-modify-write sequence is not atomic. Use AtomicInteger, synchronized, or another locking mechanism.

Static Final Fields as Constants

The safest form of a static field is static final. Such a field is assigned once during class initialization and cannot be reassigned. For primitive types and String, the compiler may inline the value at compile time, which means the constant is copied into the bytecode of referencing classes.

public class Limits { public static final int MAX_RETRIES = 3; public static final String DEFAULT_ENCODING = "UTF-8"; }

static final fields that reference mutable objects are not fully immutable. The reference cannot be changed, but the object itself can be modified:

public class Registry { public static final List<String> NAMES = new ArrayList<>(); }

Callers can call NAMES.add(...) even though NAMES is final. If the list must be truly immutable, use List.of() or wrap it in Collections.unmodifiableList().

Common Anti-Patterns with Mutable Static State

Mutable static fields are a frequent source of production issues. A few patterns stand out.

A static collection that is written from multiple threads without synchronization can corrupt its internal structure or produce inconsistent reads. Even a single-threaded application can suffer if the static state is mutated during request handling and never reset.

Static fields that hold per-request or per-user data are especially dangerous. If one request writes user-specific information into a static field, another request may read that same field and see the wrong data. This is a classic cause of cross-request contamination in web applications.

Static state also complicates testing. A test that mutates a static field can affect other tests running in the same JVM, especially when tests run in parallel. Test frameworks typically do not isolate static state between tests unless the class loader is replaced.

Alternatives to Static Mutable State

When a value must be shared but should not be global, consider one of these approaches.

Dependency injection passes the shared object through constructors or method parameters. The object is created once at the composition root and passed explicitly, which makes the dependency visible and testable.

public class Service { private final Cache cache; public Service(Cache cache) { this.cache = cache; } }

An enum-based singleton is a safe way to hold a single instance when a true singleton is required:

public enum CacheHolder { INSTANCE; private final Cache cache = new Cache(); public Cache getCache() { return cache; } }

ThreadLocal is appropriate when each thread needs its own copy of a value, such as a per-request transaction context. It avoids shared state entirely but must be cleared after use to prevent memory leaks in thread-pooled environments.

The decision comes down to the scope of the data. If the value is a constant, use static final. If it is shared configuration that never changes after startup, initialize it once and treat it as read-only. If it varies per component, per request, or per thread, keep it out of static fields entirely.

java static fields: Practical Usage and Code Examples | RYUSLOG DEV