虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
概述
Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会对类加载时增加一些性能消耗,但是提供高度的灵活性。动态扩展的语言特性就是以来运行期动态加载和动态连接这个特点实现的。
- 编写一个面向接口的应用程序,可以等到运行时再指定实际的实现类;可以通过预定义或自定义的类加载器,让一个本地的应用程序可以从网络或其他地方加载一个二进制流作为程序代码的一部分。
约定
类:类、接口
Class文件:二进制字节流
类加载时机
加载到虚拟机内存开始,到卸载出内存,整个生命周期包括
- 加载Loading
- 连接Linking
- 验证Verification
- 准备Preparation
- 解析Resolution
- 初始化Initialization
- 使用Using
- 卸载Unloading
主动引用
加载、验证、准备、初始化和卸载的开始顺序是确定的,解析则不一定,在某些情况下可以在初始化阶段之后再开始(为了支持Java语言的运行时绑定,也称动态绑定或晚期绑定)。这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用、激活另外一个阶段。
虚拟机规范没有约束何时进行类加载,但是严格规定了初始化的时机(加载、验证、准备在这之前开始了),有且只有
- 遇到
new
、getstatic
、putstatic
或invokestatic
指令时- 如果类没有进行过初始化,则先触发其初始化
- 使用new关键字实例化对象
- 读取或设置一个类的静态字段(
final
修饰,已在编译器把结果放入常量池的静态字段除外) - 调用一个类的静态方法
- 使用
java.lang.reflect
包的方法对类进行反射调用 - 初始化一个类,如果其父类没有初始化,先触发父类的初始化
- 虚拟机启动时,初始化主类(
main()
) - 使用动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果是REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法举兵,并且对应的类没有进行过初始化,则先触发具柄对应类的初始化
被动引用
除主动引用之外,所有引用类的方式都不会触发其初始化,称为被动引用
1 | public class SuperClass { |
- 通过其子类来引用父类中定义的静态字段,只会触发父类的初始化,子类不会被初始化,对于是否触发子类的加载和验证,取决于具体实现。Sun HotSpot可通过
-XX:+TraceClassLoading
观察。 - 通过数组定义来引用类,不会触发父类初始化,虚拟机会自动生成、直接继承
java.lang.Object
的子类[Lcom/bai/SuperClass
类的初始化,创建动作由字节码指令anewarray
触发,这个类代表一个元素类型为com.bai.SuperClass
的一维数组,数组中应有的属性和方法(可使用的只有被修饰为public
的length
属性和clone()
方法)都被实现在这个类里,也封装了数组元素的访问方法,当检查数组越界时回抛出java.lang.ArrayIndexOutOfBoundsException
异常
1 | public static void main(java.lang.String[]); |
- 打印常量值时并没有触发定义常量类的初始化(输出ConstClass init),虽然在Java源码中引用了
ConstClass
类中的常量HELLOWORLD
,但其实在编译阶段通过常量传播优化,已经将此常量的值hello world
存储到了NotInitialization
类的常量池中,这两个类在编译成Class之后就不存在任何联系了。
编译器仍然会为接口生成<clinit>()
类构造器,用于初始化接口中所定义的成员变量,接口在初始化时,不要求其父接口全部都完成了初始化,只有在真正使用到父接口时(引用接口中定义的常量)才会初始化。