类初始化阶段是类加载过程的最后一步。在类加载过程中,除了在加载阶段可以通过自定义加载器参与之外,其余动作完全由虚拟机主导和控制,到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。
虚拟机规范严格规定了有且只有5种情况必须立即对类进行初始化:
- 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类尚未初始化,则需要先触发其初始化。即:使用new关键字实例化对象时,读取或设置一个类的静态字段时(final修饰、已在编译期把结果放入常量池的静态字段除外),调用一个类的静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类尚未初始化,则需要先触发其初始化。
- 初始化一个类的时候,发现其父类尚未初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个主类。
- 使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类尚未初始化,则需要先触发其初始化。
这5种场景中的行为称为对一个类进行主动引用;除此之外所有引用类的方式都不会触发初始化,称为被动引用。
public class Test {
static class Sup {
public static int i = 1;
static {
System.out.println("sup init");
}
}
static class Sub extends Sup {
static {
System.out.println("sub init");
}
}
public static void main(String[] args) {
System.out.println("Test main: " + Sub.i);
}
}
使用子类引用调用父类的静态字段,不会触发子类的初始化。输出结果:
sup init
Test main: 1
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
public class MyTest {
static class Sup {
public final static int I = new Integer(1);
public final static int J = 2;
static {
System.out.println("sup init");
}
}
public static void main(String[] args) {
System.out.println("Test main: " + Sup.I);
// System.out.println("Test main: " + Sup.J);
}
}
输出结果:
sup init
Test main: 1
注释代码输出结果:
Test main: 2
在准备阶段,类变量被系统设置了初始值(类型变量的默认零值),而在初始化阶段,则根据程序代码的设置,进行初始化类变量和其他资源。或者说:初始化阶段是执行类构造器<clinit>()方法的过程。
<clinit>()方法执行过程中可能会影响程序运行行为的特点及细节:
- <clinit>()方法由编译器自动收集类中所有的类变量赋值动作和静态语句块(static{...})语句合并产生;收集顺序由语句在源文件中出现的顺序所决定。静态语句块能够访问在其之前定义的变量,在其之后定义的变量,可以赋值,但无法访问。
public class Test { static { // 给变量赋值可以编译通过 i = 0; // 编译器提示:Cannot reference a field before it is defined 非法向前引用 System.out.println("i: " + i); // 可以编译通过 System.out.println("i: " + Test.i); } private static int i = 1; }
- <clinit>()方法与类的构造函数(或者说:实例构造器<init>()方法)不同,它不需要显示的调用父类构造器,虚拟机会确保在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。java.lang.Object是虚拟机中第一个执行<clinit>()方法的类。父类的<clinit>()方法先执行,意味着父类定义的静态语句块要优先于子类的变量赋值操作。
public class Test { static class Sup { public static int i = 1; static { System.out.println("sup static before: " + i); i = 2; System.out.println("sup static after: " + i); } } static class Sub extends Sup { public static int j = 3; static { System.out.println("sub static before: " + j); j = 4; System.out.println("sub static after: " + j); } } public static void main(String[] args) { System.out.println("Test main sub j: " + Sub.j); } }
输出结果: sup static before: 1 sup static after: 2 sub static before: 3 sub static after: 4 Test main sub j: 4
<clinit>()方法对于类或接口不是必须的。一个类中没有静态代码块,也没有对变量的赋值操作,编译器可以不为类生成。
接口中不能使用静态语句块,但可以有变量初始化的赋值操作,因此接口也与类一样都会生成<clinit>()方法。但接口与类不同的是:执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量使用时,才会初始化。接口的实现类在初始化时也不会执行接口的<clinit>()方法。
虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时初始化一个类,只有一个线程会执行这个类的<clinit>()方法,其他线程都会阻塞等待,直到活动线程执行<clinit>()方法完毕,其他线程唤醒后也不会再次执行<clinit>()方法。同一个类加载器下,一个类型只会初始化一次。
本文内容主要来源与《深入理解Java虚拟机》第2版 第7章(7.2 类加载的时机、7.35 初始化),用于学习、归纳、总结。