static, final and Class Initialisation Order
The exact order the JVM initialises a class, why a static final String can survive deleting the class that declared it, effectively final, and how two classes can deadlock while loading.
On this page
This topic looks like trivia and is not. Initialisation order explains a whole family of real bugs:
a NullPointerException in a constructor that reads a field which "obviously" has a value, a
configuration constant that refuses to change after a partial redeploy, and a service that hangs on
startup with two threads blocked inside class loading.
Key Takeaways
- A class is initialised lazily, on first active use, and exactly once — the JVM holds a lock to guarantee it.
- Static initialisers run in source order. Reading a static field declared below the block that reads it gives you the default value, not the assigned one.
- Superclass constructors complete before subclass fields are initialised. Calling an overridable method from a constructor is therefore unsafe.
static finalwith a constant expression is inlined at every call site and no longer read at runtime.- Two classes whose static initialisers reference each other can deadlock on two threads.
Load, link, initialise
The step people forget is preparation. Before any of your code runs, every static field already exists holding its type's default. Only later does initialisation execute the assignments in source order. That gap is directly observable:
public class Ordering {
static int a = compute("a", 1);
static {
System.out.println("block sees b = " + b); // prints 0, not 2
b = 99;
}
static int b = compute("b", 2); // overwrites the 99
static int compute(String name, int v) {
System.out.println("assigning " + name);
return v;
}
public static void main(String[] args) {
System.out.println("b = " + b); // 2
}
}Reading b from a block declared above it is legal (assignment is allowed; only reading by simple
name is restricted, and the qualified form Ordering.b sidesteps even that) and yields 0. Then the
declaration further down assigns 2, wiping out the 99. Keeping static state in source order and
avoiding forward references is the whole defence.
Initialisation is triggered by active use: new, calling a static method, assigning or reading a
non-constant static field, reflection, or initialising a subclass. Reading a constant static
field does not trigger it — see below.
Instance initialisation, and the constructor trap
class Base {
Base() {
init(); // calls the override
}
void init() { }
}
class Derived extends Base {
private final List<String> items = new ArrayList<>();
private int limit = 10;
@Override void init() {
items.add("from Base constructor"); // NullPointerException
System.out.println(limit); // would print 0
}
}The order for new Derived() is: Base constructor runs to completion → Derived's field
initialisers run → Derived's constructor body runs. So when Base() calls init(), dynamic
dispatch sends it to Derived.init(), where items is still null and limit is still 0.
Instance initialiser blocks — a bare { } in the class body — run with the field initialisers, in
source order, before the constructor body. They are rarely worth using; the one legitimate case is
sharing setup across several constructors, and even then a private helper is clearer.
final, and the three things it means
| Applied to | Means |
|---|---|
| Local variable | Assigned once. Enables capture in a lambda without the keyword since Java 8 |
| Field | Assigned once, in the declaration or every constructor. Gives safe publication |
| Method | Cannot be overridden |
| Class | Cannot be extended |
| Parameter | Cannot be reassigned inside the method |
The one with real teeth is final on a field. The Java Memory Model guarantees that any thread
which sees a correctly constructed object also sees the final values of its final fields, with no
synchronisation. This is the guarantee that makes immutable objects safe to publish through a data
race — and it applies only to final fields, which is why "make it immutable" means "make every
field final", not "just do not write a setter".
Effectively final is the compiler noticing that a local variable is never reassigned. Since Java 8 you can capture such variables in a lambda or anonymous class without writing the keyword:
String prefix = "order-"; // effectively final — capture is fine
list.forEach(x -> print(prefix + x));
int counter = 0;
list.forEach(x -> counter++); // does not compile: not effectively finalThe restriction exists because the lambda captures the value, not the variable. Allowing
reassignment would make it ambiguous which value the lambda sees. The standard workaround —
an AtomicInteger or a one-element array — is usually a sign the loop should have been a
reduce or a count().
The constant that will not change
public class Config {
public static final String VERSION = "2.4.0"; // compile-time constant — INLINED
public static final int TIMEOUT_MS = 30 * 1000; // also inlined
public static final Duration TIMEOUT = Duration.ofSeconds(30); // NOT inlined
public static final String BUILT_AT = System.getenv("BUILD_TS"); // NOT inlined
}A static final field of a primitive or String type, initialised with a constant expression, is a
compile-time constant. Every class that reads it gets the literal copied into its own constant pool
at compile time.
The consequence catches people out: rebuild Config alone with VERSION = "2.5.0", redeploy that
one jar, and every other class still reports 2.4.0. You can even delete Config entirely and the
callers keep working. The fix is a full recompile, or — better — not making a value that changes into
a compile-time constant. Wrapping it (Duration.ofSeconds(30), a method call, anything non-constant)
defeats the inlining deliberately.
Class initialisation deadlock
Initialisation is guarded by a per-class lock, and that lock is held for the whole static initialiser. Two classes that reference each other during initialisation, touched first by two different threads, deadlock:
class Alpha {
static final Alpha INSTANCE = new Alpha();
static final int FROM_BETA = Beta.VALUE; // needs Beta initialised
static final int VALUE = 1;
}
class Beta {
static final Beta INSTANCE = new Beta();
static final int FROM_ALPHA = Alpha.VALUE; // needs Alpha initialised
static final int VALUE = 2;
}Thread 1 touches Alpha and takes its lock; thread 2 touches Beta and takes its lock; each then
waits for the other. The process hangs with no exception, and a thread dump shows both threads in
Class.forName or a static initialiser. Single-threaded startup hides it entirely, which is why it
tends to appear only under production load.
The defence is to keep static initialisers trivial. No cross-class references, no I/O, no thread creation. If initialisation genuinely needs work, do it lazily behind a holder class — the initialisation-on-demand idiom — where the JVM's own guarantee gives you thread safety with no lock of your own:
public class Registry {
private Registry() { }
private static class Holder {
// Initialised on first access to Holder.INSTANCE, once, by the JVM.
static final Registry INSTANCE = new Registry();
}
public static Registry getInstance() { return Holder.INSTANCE; }
}Frequently Asked Questions
In what order do static blocks, instance blocks and constructors run?
What is the difference between final and effectively final?
Why does changing a public static final String require recompiling callers?
Related tutorials
- Exception Handling, finally and try-with-resourcesChecked versus unchecked and when each is right, how finally silently discards an exception or a return value, and suppressed exceptions in try-with-resources.
- Generics, Type Erasure and WildcardsWhat the compiler removes and what it inserts, why you cannot create an array of a generic type, PECS explained by what it enables, bridge methods, and heap pollution from unchecked varargs.
- OOP Principles the Interviewer Actually ProbesThe OOP questions behind the textbook four: static vs dynamic dispatch, Liskov violations that compile cleanly, abstract class versus interface, and composition over inheritance.
- Immutable Objects & Defensive CopyingThe five conditions for a genuinely immutable class, the leaked-collection bug that defeats final, why records are only shallowly immutable, and what final fields guarantee across threads.