In the JVM, when exactly does a Java `static int counter = 5;` field actually hold the value 5 — and how is `static final int LIMIT = 100;` treated differently by the class file and the runtime?
answer
- Prepare = allocate + zero default
- = 5 lives in the class initializer, runs later
- ConstantValue attribute set during preparation
- javac inlines constants into readers
- Stale constant after partial recompile
basics
~20 sDuring preparation the field is allocated and set to 0; the value 5 is only written when the class is initialized and its static initializer runs. A compile-time constant like static final int LIMIT = 100 carries a ConstantValue attribute and is set during preparation — and javac also copies the literal into every class that reads it.
solid answer
~50 sPreparation allocates storage for static fields and stamps the type's default value: `0` for `int`. The assignment `= 5` is compiled into the class initializer method and runs during **initialization**, which happens later. Between the two, the field is observably `0` — reachable, for example, when a circular static-initialization cycle reads the class mid-way. `static final int LIMIT = 100` is a *compile-time constant*: its initializer is a constant expression, so javac emits a `ConstantValue` attribute for the field, and preparation writes 100 straight away — no class-initializer code involved. There is a second, sharper consequence. Because the value is a compile-time constant, javac inlines the literal into every class that reads it, so the reading class's bytecode contains `100` and never even references the declaring class. Change the constant, recompile only the declaring class, and consumers keep the stale value until they are recompiled too.
code
text · 12 linesstatic final int LIMIT;
descriptor: I
flags: ACC_STATIC, ACC_FINAL
ConstantValue: int 100 <-- assigned during preparation
static int counter;
descriptor: I
flags: ACC_STATIC <-- no ConstantValue; 0 after preparation
static {}; // class initializer, runs at initialization
0: iconst_5
1: putstatic #2 // Field counter:Igo deeper
Recall that statics are zeroed first and get their real values later, and that a static final int with a literal value is special.
Explain the ConstantValue attribute versus assignment in the class initializer, and that the field is observably 0 in between.
Add the consumer-side inlining and the stale-constant hazard across separately compiled artifacts, plus how to inspect it with javap.
Treat constant-inlining as an API compatibility rule: public compile-time constants are effectively frozen into every consumer build, so choose the declaration form according to whether the value must be changeable independently.
## Two different fields, two different timelines ```java class Config { static int counter = 5; // ordinary static static final int LIMIT = 100; // compile-time constant static final String NAME = "svc"; // also a compile-time constant static final Integer BOXED = 100; // NOT a compile-time constant } ``` **`counter`.** javac cannot express `= 5` in the field's metadata, so it emits the assignment as a statement inside the synthetic class-initializer method the JVM invokes during initialization. Preparation, which runs earlier as part of linking, only allocates the field and writes the default for `int`, which is `0`. So there is a real window during which `Config.counter == 0`. That window is not merely theoretical. If class `A`'s static initializer triggers `B`'s, and `B`'s initializer reads back into `A`, the JVM does not re-run `A`'s initializer — it lets `B` observe `A`'s fields in whatever state they have reached, which for fields not yet assigned means the prepared defaults. Every "my static was null even though I initialized it" puzzle is this timeline. **`LIMIT`.** Its initializer is a constant expression of a primitive type (or `String`), and it is `static final`, so the language classifies it as a compile-time constant. javac records the value in the field's `ConstantValue` attribute, and the JVM's preparation phase assigns it from there. No initializer code is needed for that field. **`BOXED`.** `Integer` is not a primitive or `String`, so despite `static final` it is not a compile-time constant; it is assigned during initialization like `counter`. ## The constant-inlining consequence The deeper effect is on *readers*. When a class reads a compile-time constant, the language rules let the compiler substitute the value at the point of use. The reading class file therefore contains the literal `100` in its own constant pool, with no field reference to `Config` at all. Two practical results: 1. **Stale constants across separate compilation.** Change `LIMIT` to 200 and recompile only `Config`; a consumer class compiled earlier still executes with 100 until it is recompiled. This is a classic source of "impossible" bugs in builds that do not recompile everything, and of subtle mismatches when a library ships new constants without consumers rebuilding. 2. **No reference means no triggering.** Reading a compile-time constant does not cause the declaring class to be initialized, because there is no reference to it in the reader's bytecode at all. That is why some constants can be read from a class whose static initializer would have thrown. ## Checking it yourself `javap -v -p` shows the difference immediately: the constant field carries a `ConstantValue` attribute, and the ordinary static's assignment appears as bytecode inside the class-initializer method. A consumer's bytecode shows `ldc` / `bipush` of the literal where you might have expected a `getstatic`. ## What to take away Preparation is about *storage and defaults*, not about the values you wrote. Only compile-time constants — `static final` primitives and `String`s with constant initializers — are handled at that point, and their compile-time inlining into readers is a build-hygiene concern, not only a runtime one. If a constant must be changeable without recompiling consumers, do not make it a compile-time constant: compute it, or hold it in a non-constant type.
- Why can changing a public static final int in a library and recompiling only that library leave callers using the old value?Because a static final primitive with a constant initializer is a compile-time constant, and the compiler substitutes its literal value directly into each reading class. The caller's bytecode contains the old number and no reference to the library field at all, so shipping a new library jar changes nothing until the callers are recompiled.
- How would you declare a constant so that consumers always see the current value without recompiling?Prevent it from being a compile-time constant. Give it a non-constant initializer, for example computing it or reading it from configuration, or use a non-primitive, non-String type. Then the reader's bytecode holds a real field reference resolved at run time, and the value is picked up from whatever version of the declaring class is loaded.
saying these in an interview costs you the question
- Saying the static holds its source value immediately after preparation
- Thinking any static final field is a compile-time constant, including boxed types and objects
- Not knowing that compile-time constants are copied into reading classes
- Assuming a stale constant means the build system is broken rather than the language rule