Static and instance
What static really means, when a static factory beats a constructor, and the singleton that is almost always a mistake.
static means "belongs to the class, not to any object". That is the entire definition, and yet it is misused in two opposite directions: people write instance methods that never touch instance state, and they write static state that should have been an object. The lesson is knowing which side of the line a thing belongs on, where a static field actually lives, when the class's initialiser runs and what happens when two of them wait for each other, and why "one per JVM" is not what static promises.
Static members
A static field exists once per class, per class loader, regardless of how many instances exist — or whether any do. A static method has no this; it can read static fields and its parameters, nothing else.
public final class Money {
public static final Money ZERO = new Money(0, Currency.EUR); // one shared constant
private static final Pattern FORMAT = Pattern.compile("..."); // one compiled regex
private final long minor;
private final Currency currency;
public static Money of(long minor, Currency c) { return new Money(minor, c); } // factory
public Money plus(Money other) { ... } // instance
}Legitimate static things: constants, pure functions (Math.max, Objects.requireNonNull), factories, and expensive immutable objects worth sharing — a compiled Pattern, a DateTimeFormatter, a Jackson ObjectMapper.
Under the hood: where statics live, and when they are born
A static field is stored in the class's mirror, the java.lang.Class object on the heap (since Java 7; before that, in the permanent generation). A class is loaded once per class loader, so there is one mirror and one set of statics per loader, not per JVM. An application server with two web apps has two Config.class objects and two Config.instance fields, and a plugin system that reloads a jar gets a fresh set of statics with the new loader while the old ones stay alive as long as anything references the old class.
A static method call compiles to invokestatic, a direct call with no receiver and no dispatch, which is why the JIT inlines them freely and why they cannot be overridden. Static fields are read with getstatic, and the first getstatic, putstatic, invokestatic or new on a class triggers its initialisation: the JVM takes the class's init lock, runs <clinit> (every static field initialiser and static {} block, concatenated in textual order by javac), and marks the class initialised. Every other thread that touches the class meanwhile blocks on that lock. Two consequences:
- Lazy, and thread-safe by the JVM. The holder idiom,
static class Holder { static final Expensive I = new Expensive(); }, is a lazy singleton with novolatileand no double-checked locking, because the init lock does the work once. - Initialisation can deadlock. Thread A initialises class
Foo, whose<clinit>touchesBar; thread B is at the same moment initialisingBar, whose<clinit>touchesFoo. Each waits for the other's init lock, forever, and a thread dump shows both threadsRUNNABLEin a static initialiser with no monitor in sight. Cyclic static initialisers between classes are the cause, and keeping<clinit>trivial is the cure.
A compile-time constant (static final of a primitive or String, initialised with a constant expression) is not a getstatic at all: javac inlines its value into every class that uses it. Change MAX = 100 to 200 in a library and every caller compiled against the old jar keeps 100 until it is recompiled. That is the one case where "changing a constant" is a binary-incompatible change.
Static factories over constructors
Money.of(500, EUR) reads better than new Money(500, EUR), and a factory can do things a constructor cannot:
- Have a name.
LocalDate.of(2024, 1, 31),LocalDate.parse("2024-01-31"),LocalDate.now()— three factories, three meanings, one class. - Return a cached instance.
Integer.valueOf,Boolean.valueOf,Money.ZERO. - Return a subtype.
List.of()returns a different class for zero, one, two or many elements;EnumSet.ofpicks a bit-vector implementation by enum size. The caller sees the interface. - Refuse. A constructor must return an object; a factory can return
Optional.empty().
The convention: of for a value, from for a conversion, valueOf for parsing, create/newInstance for a fresh object each time, getInstance for one that may be shared.
Static state is global state
A static mutable field is a global variable with a class name in front of it. Everything that is wrong with globals applies: every test shares it, every thread races on it, and nothing in a method signature reveals that it depends on it.
public class RequestContext {
private static String currentUser; // shared by every thread in the JVM
public static void set(String u) { currentUser = u; }
public static String get() { return currentUser; }
}Two concurrent requests overwrite each other's user. The bug is invisible in single-threaded tests. The fixes, in order of preference: pass the value as a parameter; hold it in an object whose lifetime matches the request; use a ScopedValue (Java 25) or a ThreadLocal only when the framework gives you no other hook, and clear it when the request ends.
Walkthrough: the date that was wrong one time in a thousand
A reporting service formatted timestamps with private static final SimpleDateFormat FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm"). It passed every test. In production, under 200 concurrent requests, about one row in a thousand carried a date from a different request, and occasionally a NumberFormatException came out of parse.
SimpleDateFormatkeeps its working state in instance fields: aCalendarit fills in duringformatand reads duringparse. It is documented as not synchronised.- Thread A calls
FMT.format(t1); it sets the calendar's fields fromt1. Thread B callsFMT.format(t2)at the same moment and sets the same calendar. Thread A reads the fields back to build its string and gets some oft2's. - The wrong dates were valid dates, so nothing failed; the
NumberFormatExceptionappeared only whenparseread a half-written internal buffer. staticmade one instance shared by every thread; the class being mutable made that a race. Either half alone is fine: a static immutableDateTimeFormatter, or a per-callnew SimpleDateFormat.- Fix:
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"), which is immutable and therefore shareable. The test that would have caught it is the latch-and-loop test from the Concurrency course, formatting from ten threads and asserting every result parses back to its input.
The singleton
The singleton pattern — a class with a private constructor and a static getInstance() — is static state with extra ceremony. It has the same problems: global, hard to test, hidden dependency. It is also one-per-class-loader, not one-per-JVM, which application servers demonstrate on every redeploy. In a Spring application it is redundant besides: a @Component is a singleton scoped to the container, injected where it is needed, replaceable in a test, and created after its own dependencies. If you find yourself writing getInstance(), you almost certainly want a bean.
The one honest singleton is an enum with a single constant, for something that genuinely is one-per-loader and stateless; it is also the only singleton that survives serialisation and reflection intact. Even then, prefer injecting it.
Static methods and testing
A static method cannot be overridden and cannot be replaced by a mock without bytecode tricks. That is fine for pure functions — you do not mock Math.max. It is a problem for anything that touches the outside world: Clock.now(), FileSystem.read(), HttpClient.get(). Make those instance methods on an object you inject, so a test can substitute a fixed clock or a fake client. java.time designed this in: LocalDate.now(Clock) exists so you can pass Clock.fixed(...). Mockito's mockStatic exists for legacy code and works by rewriting the class's bytecode for the duration of a try-with-resources; it is a tool for code you cannot change, not a design.
Static nested and inner classes
A static nested class is just a class that lives inside another for namespacing; it has no reference to an outer instance. A non-static inner class captures a hidden reference to the outer object, a synthetic this$0 field that javap shows, which keeps the outer object alive as long as the inner one exists, and is the source of a well-known memory leak with listeners and anonymous classes. Lambdas capture this only if they use it; anonymous inner classes always do. Default to static for nested classes; drop it only when the inner class genuinely needs the outer instance.
Initialisation order
Static fields and static initialiser blocks run once, in textual order, when the class is first initialised. Instance fields and initialiser blocks run before the constructor body, in textual order. A static field that references another class's static in a cycle sees null or 0 where it expected a value, because the other class's <clinit> has started but not finished, and the JVM lets the initialising thread through its own lock. Keep static initialisers trivial.
Try it yourself
What prints?
class A { static final String X = "x"; static String Y = "y"; static { System.out.println("A init"); } }
class Main {
public static void main(String[] a) {
System.out.println(A.X);
System.out.println("--");
System.out.println(A.Y);
System.out.println(new A[3].length);
}
}Answer
x, --, A init, y, 3. A.X is a compile-time constant inlined into Main; reading it does not touch A. A.Y is a getstatic, which initialises A first (A init). Creating an array of A loads the class but does not initialise it, and it is already initialised anyway. Make X non-final, or initialise it from a method, and A init prints first.
Two singletons
A library's Registry.getInstance() is called from two web applications deployed in one Tomcat, and each application sees different registrations. The library author insists the class is a singleton. Who is right?
Answer
Both, about different things. The class is a singleton per class loader; Tomcat gives each web application its own loader, and the library jar is in each application's WEB-INF/lib, so there are two Registry classes, two static fields, two instances. To share one, the jar must live in a common parent loader (Tomcat's lib/), and then its statics are shared by every application, which is a different problem.
Find the leak
class Window {
private final byte[] buffer = new byte[10_000_000];
Runnable onClose() { return new Runnable() { public void run() { log("closed"); } }; }
}The Runnable is stored in a long-lived list after the window is gone. Why does memory grow, and what is the one-word fix?
Answer
The anonymous class is an inner class: it holds a synthetic this$0 reference to the Window, and the Window holds a 10 MB buffer, so every stored Runnable pins 10 MB. The fix is a lambda, return () -> log("closed");, which captures this only when the body uses it, and this body does not. If the class must stay a class, make it static nested.
Misconceptions
- "
staticmeans one per JVM." One per class per class loader. Two loaders, two copies. - "Static initialisation is a one-time cost with no risks." It takes a class-level lock; a cycle between two classes' initialisers deadlocks two threads with no monitor to see in the dump.
- "A
static finalconstant is safe to change in a library." Its value is inlined into every caller at compile time. Callers keep the old value until recompiled. - "Static methods are faster, so prefer them." They are direct calls and the JIT inlines instance methods on monomorphic sites just as well. Choose by whether there is a
this, not by speed. - "Anonymous classes and lambdas are the same." An anonymous inner class always captures the outer instance; a lambda captures it only if it uses it. The difference is a memory leak.
Going deeper
- JLS §12.4 (initialisation of classes) and JVMS §5.5, which specify the init lock and the recursive-initialisation rule that makes cycles see defaults.
- JLS §15.28 (constant expressions) and §13.4.9 for why inlined constants are a binary-compatibility trap.
- Effective Java, items 1 (static factories), 3 (singletons), 24 (static member classes over non-static).
java.lang.ClassJavadoc on static fields and the class mirror;jolto seethis$0in an inner class's layout.- Mockito's
mockStaticdocumentation, for what it does to the class and why it is a last resort.