For static constants (final) fields, the .class file defines the constant value directly, so the JVM can assign it when the class is loaded.
For non-constant static fields, the compiler will merge any initializers with custom static initializer blocks to produce a single static initializer block of code, that the JVM can execute when the class is loaded.
Example:
public final class Test {
public static double x = Math.random();
static {
x *= 2;
}
public static final double y = myInit();
public static final double z = 3.14;
private static double myInit() {
return Math.random();
}
}
Field z
is a constant, while x
and y
are run-time values and will be merged with the static initializer block (the x *= 2
).
If you disassemble the bytecode using javap -c -p -constants Test.class
, you will get the following. I've added blank lines to separate the merged sections of the static initializer block (static {}
).
Compiled from "Test.java"
public final class test.Test {
public static double x;
public static final double y;
public static final double z = 3.14d;
static {};
Code:
0: invokestatic #15 // Method java/lang/Math.random:()D
3: putstatic #21 // Field x:D
6: getstatic #21 // Field x:D
9: ldc2_w #23 // double 2.0d
12: dmul
13: putstatic #21 // Field x:D
16: invokestatic #25 // Method myInit:()D
19: putstatic #28 // Field y:D
22: return
public test.Test();
Code:
0: aload_0
1: invokespecial #33 // Method java/lang/Object."<init>":()V
4: return
private static double myInit();
Code:
0: invokestatic #15 // Method java/lang/Math.random:()D
3: dreturn
}
Note that this also shows that a default constructor was created by the compiler and that the constructor calls the superclass (Object
) default constructor.
UPDATE
If you add the -v
(verbose) argument to javap
, you'll see the constant pool which stores the values defining those references listed above, e.g. for the Math.random()
call, which is listed above as #15
, the relevant constants are:
#15 = Methodref #16.#18 // java/lang/Math.random:()D
#16 = Class #17 // java/lang/Math
#17 = Utf8 java/lang/Math
#18 = NameAndType #19:#20 // random:()D
#19 = Utf8 random
#20 = Utf8 ()D
As you can see, there is a Class constant (#16) for the Math
class, which is defined as the string "java/lang/Math"
.
The first time reference #16 is used (which happens when executing invokestatic #15
), the JVM will resolve it to an actual class. If that class has already been loaded, it'll just use that loaded class.
If the class has not yet been loaded, the ClassLoader
is invoked to load the class (loadClass()
), which in turn calls the defineClass()
method, taking the bytecode as a parameter. During this loading process, the class is initialized, by automatically assigning constant values and executing the previously identified static initializer code block.
It is this class reference resolving process performed by the JVM that triggers the initialization of the static fields. This is essentially what happens, but the exact mechanics of this process is JVM implementation specific, e.g. by JIT (Just-In-Time compilation to machine code).