对于静态常量(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
3: putstatic #21
6: getstatic #21
9: ldc2_w #23
12: dmul
13: putstatic #21
16: invokestatic #25
19: putstatic #28
22: return
public test.Test();
Code:
0: aload_0
1: invokespecial #33
4: return
private static double myInit();
Code:
0: invokestatic #15
3: dreturn
}
请注意,这也表明编译器创建了一个默认构造函数,并且该构造函数调用了超类(
Object
)的默认构造函数。
更新
如果您在javap
命令后添加-v
(详细)参数,您将看到常量池,其中存储了上面列出的定义这些引用的值,例如对于上面列出的Math.random()
调用,即#15
,相关的常量为:
正如您所看到的,
Math
类有一个类常量(#16),该常量被定义为字符串
"java/lang/Math"
。
当第一次使用引用#16时(即执行
invokestatic #15
时),JVM将其解析为一个实际的类。如果该类已经被加载,它将直接使用该加载的类。
如果尚未加载该类,则会调用
loadClass()
来加载该类的
ClassLoader
,该方法将字节码作为参数传递给
defineClass()
方法。在此加载过程中,通过自动分配常量值并执行先前确定的静态初始化器代码块来初始化该类。
正是由JVM执行的这个类引用解析过程触发了静态字段的初始化。这基本上就是发生的情况,但是这个过程的确切机制是JVM实现特定的,例如通过JIT(即时编译为机器代码)。