Java虚拟机类加载的初始化
我们知道,《Java虚拟机规范》章节5.5 Initialization ^(1)^ 中严格规定了有且只有六种情况必须立即对类进行初始化:
- 遇到new(实例化对象)、getstatic(读取一个没有被final修饰、没有在编译期把结果放入常量池的类的静态字段)、putstatic(设置一个没有被final修饰、没有在编译期把结果放入常量池的类的静态字段)或者invokestatic(调用一个类的静态方法)这四条字节码指令时,如果类没有进行初始化则需要触发初始化。
- 使用java.lang.reflect包的方法对类型进行反射调用
- 父类还没有初始化
- 虚拟机启动时,用户需要制定一个要执行的主类(main)
- 当使用java7新加入的动态语言支持时,如果一个MthodHandle实例最后的解析结果是REF_getStatic、REF_putstatic、REF_invokestatic、REF_newInvokeSpecial四种类型的方法句柄
- 一个接口定义了default修饰的接口方法,同时接口的实现类发生了初始化
除此之外,所有引用类型的方式都不会触发初始化,称为被动引用:
子类引用父类
通过子类引用父类的静态字段不会导致子类初始化:
1 |
|
结果是什么呢?会先输出“Super”,然后是value的值。
对于静态字段,只有直接定义这个字段的类才会被初始化,所以通过子类引用父类静态字段,也只有父类会初始化。
数组引用类
通过数组定义来引用类,不会触发此类的初始化
1 |
|
结果是什么呢?
什么也没有。
可见Sub类并没有被初始化。不过这段代码触发了另一个名为“[Lcom.example.demo.Sub”的类的初始化阶段。这玩意是虚拟机自动生成的直接继承自Object的子类,创建动作由字节码指令anewarray(即新建引用数组)触发。
这个类代表的一维数组,用户可直接使用的只有被修饰为Public的length属性和clone方法,当然数组中应用的属性和方法都实现在这个类中。这是因为Java包装了数组元素的访问(在C中是数组指针的移动),这也就是为什么Java检测到数组越界会抛出ArrayIndexOutOfBoundsException异常而不是像C中的非法内存访问。
准确的说,Java的越界检查不是封装在数组元素访问的类中,而是封装在数组访问的xaload(数组的元素压栈)、xastore(针对数组的操作)字节码指令。
常量池引用
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类
1 |
|
这里的代码运行后也不会输出“Super”。
hello的常量值其实在编译阶段已经被转化成SomeName类对自身常量池的引用了,所以他们俩没什么关系了。
接口
接口和类有一点不同。接口其实也有初始化过程,不过接口不能像类一样用static代码块来输出初始化信息,编译器会为接口生成“
接口不要求父接口全部初始化,只有用到了才会初始化。
引用
(1) Yellin, F. and Lindholm, T., 1996. The java virtual machine specification.