Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

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.

Intermediate6 min readUpdated
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 final with 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.
Preparation assigns defaults — 0, false, null. Initialisation runs your assignments and static blocks, once, on first active use.

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:

Ordering.java
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

the classic puzzle
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 toMeans
Local variableAssigned once. Enables capture in a lambda without the keyword since Java 8
FieldAssigned once, in the declaration or every constructor. Gives safe publication
MethodCannot be overridden
ClassCannot be extended
ParameterCannot 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:

capture rules
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 final

The 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

Config.java
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:

a real startup hang
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:

LazyHolder.java
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?
On first use of the class: static fields and static blocks run once, in source order. Then for each instance: the superclass constructor runs to completion first, then the subclass instance initialisers and instance field initialisers in source order, then the subclass constructor body. That means a superclass constructor can call an overridden method before the subclass fields are assigned.
What is the difference between final and effectively final?
A final variable is declared final and the compiler enforces single assignment. An effectively final variable is not declared final but is never reassigned after initialisation, so the compiler treats it as if it were. Since Java 8 a lambda or anonymous class may capture either, which is why most local variables no longer need the keyword.
Why does changing a public static final String require recompiling callers?
Because a static final field initialised with a compile-time constant expression is inlined into every class that reads it. The reading class holds a literal copy in its constant pool and never looks at the declaring class at runtime. Recompiling only the declaring class leaves every caller with the old value — a genuinely confusing production bug.

Related tutorials