对于静态常量(final)字段,.class文件直接定义了常量值,所以JVM可以在加载类的时候赋值。
对于非常量静态字段,编译器会将任何初始化程序与自定义静态初始化程序块合并,以生成单个静态初始化程序代码块,JVM 可以在加载类时执行该代码块。
例子:
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();
}
}
字段z 是一个常量,而x 和y 是运行时值,将与静态初始化块(x *= 2)合并。
如果您使用javap -c -p -constants Test.class 反汇编字节码,您将得到以下信息。我添加了空白行来分隔静态初始化块 (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
}
请注意,这也表明编译器创建了一个默认构造函数,并且该构造函数调用了超类 (Object) 的默认构造函数。
更新
如果您将-v(详细)参数添加到javap,您将看到存储定义上述引用的值的常量池,例如对于上面列出的#15 的Math.random() 调用,相关常量为:
#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
如您所见,Math 类有一个 Class 常量 (#16),它被定义为字符串 "java/lang/Math"。
第一次使用#16 引用(在执行invokestatic #15 时发生),JVM 会将其解析为实际类。如果该类已经加载,它将只使用该加载的类。
如果类尚未加载,则调用ClassLoader 加载类(loadClass()),然后调用defineClass() 方法,将字节码作为参数。在此加载过程中,通过自动分配常量值并执行先前标识的静态初始化程序代码块来初始化类。
正是JVM执行的这个类引用解析过程触发了静态字段的初始化。这基本上就是发生的事情,但是这个过程的确切机制是特定于 JVM 实现的,例如通过 JIT(即时编译为机器代码)。