类加载
类加载的时机
类加载声明周期
类从被加载到虚拟机内存开始,到卸载出内存为止,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。
其中验证、准备、解析三个部分称为连接(Liking)。
这个7个阶段的发生顺序如下图所示:
加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,是按部就班进行的,而解析阶段不一定:它在某些情况下可以在初始化之后再开始,这是为了支持Java语言的运行时绑定。
这些阶段都是互相交叉地混合式进行的,通常会在一个阶段执行过程中调用、激活另外一个阶段。
类初始何时进行
- 遇到new、getstatic、putstatic或invokestatic这4条指令时,也就是使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被 static final 修饰的、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候
- 使用java.lang.reflect包的方法对类进行反射调用的时候
- 初始化一个类的时候,如果发现其父类还没有进行初始化的时候
- 当虚拟机启动时,用户需要制定一个要执行的主类(包含main() 方法的那个类)
- 如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄的时候
注意:
一个接口初始化时,并不要求父接口全部都完成了初始化,只有在真正使用到了父接口的时候才会初始化
类加载的过程
类加载的全过程,也就是记载、验证、准备、解析和初始化这5个阶段所执行的具体动作。
加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证
文件格式验证
是否符合class文件格式的规范
- 是否以魔数0xcafebabe开头
- 主、次版本号是否在当前虚拟机处理范围之内
- 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
- 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
- CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据
- class文件中各个部分及文件本身是否有被删除的或附加的其他信息
… …
元数据验证
是否符合Java语言规范
- 这个类是否有父类(除了object类,其他的类都应该有父类)
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
- 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
- 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类final字段,或者出现不符合规则的方法重载)
… …
字节码验证
程序语义是否合法、符合逻辑
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现这样的情况:在操作数栈放置了一个int数据类型,使用时却按long类型来加载如本地变量表中
- 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法体中的类型转换是有效的
… …
符号引用验证
在解析阶段进行验证
- 符号引用中通过字符串描述的全限定名是否能找到对应的类
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
- 符号引用的类、字段、方法的访问性是否被当前类访问
… …
准备
为类变量分配内存并设置类变量初始值的阶段,在方法区进行分配。
这里所说的初始值“通常情况下”是数据类型的零值。特殊情况就是如果字段属性表中存在ConstantValue属性,那么准备阶段变量就会初始化为ConstantValue属性所指定的值。
解析
解析阶段就是将常量池内的符号引用替换为直接引用的过程。直接引用可以是指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标必定已经在内存中存在。
类或接口解析
假设当前代码所处的类为D,如果把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,那么虚拟机完成整个解析的过程需要以下3个步骤:
- 如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,有可能触发其他相关类的加载动作。一旦这个加载过程出现了任何异常,解析过程宣告失败
- 如果C是一个数组类型,并且数组的类型的元素类型是对象,则将会按照第1点的规则加载数组元素类型,接着
- 如果上面的步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了
字段解析
首先对字段所属的类或接口进行解析,如果解析成功,用C表示所属的类或接口,按照以下步骤对C进行后续字段的搜索
- 如果C本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用
- 否则,如果在C中实现了接口,则按照继承关系从下向上递归搜索各个接口和它的父接口,如果接口包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用
- 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果父类中包含了简单名称和字段描述符都与目标匹配的字段,则返回这个字段的直接引用
- 否则,查找失败
类方法解析
首先对方法所属的类进行解析,如果解析成功,用C表示所属的类,按照以下步骤对C进行后续类方法的搜索
- 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现索引的C是个接口,失败
- 如果通过第1步,在类C中查找是否有简单和描述符都与目标相匹配的方法,如果有则放回这个方法的直接引用
- 否则,在类C的父类中递归查找是否有简单和描述符都与目标相匹配的方法,如果有则放回这个方法的直接引用
- 否则,在类C实现的接口列表以及它们的父类接口之中递归查找是否有简单名称和描述符与目标相匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,失败
- 否则,失败
接口方法解析
首先对方法所属的接口进行解析,如果解析成功,用C表示所属的接口,按照以下步骤对C进行后续接口方法的搜索
- 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现索引的C是个类而不是接口,失败
- 否则,在接口C中查找是否有简单和描述符都与目标相匹配的方法,如果有则放回这个方法的直接引用
- 否则,在接口C的父接口中递归查找是否有简单和描述符都与目标相匹配的方法,如果有则放回这个方法的直接引用
- 否则,失败
初始化
初始阶段是执行类构造器<licnit>()
方法的过程。
<licnit>()
方法有编译器自动收集类中的所有类变量的复制动作和静态语句块中的语句合并产生的,收集循序是按照源文件中出现的顺序决定的<licnit>()
方法与类的构造函数(<init>()
)不同,他不需要显式地调用父类构造器,虚拟机会保证子类的<licnit>()
方法执行之前,父类的<licnit>()
方法已经执行完毕- 接口的
<licnit>()
方法不需要先执行父接口的<licnit>()
方法 - 虚拟机会保证
<licnit>()
方法在多线程环境中被正确地加锁、同步,保证<licnit>()
方法只会被执行一次
类加载器
类与类加载器
比较两个类是否相等,只有在这两个类是由同一个虚拟机加载的前提下才有意义,否则,即使这两个类来源于同一个class文件,被同一个虚拟机加载,只要加载它们的类加载不同,那么这两个类就必定不相等。
双亲委派模型
- 启动类加载器负责
lib
目录中的 - 扩展类加载器负责
lib\ext
目录中的 - 应用程序类加载器是程序中默认的类加载器
图中展示的类加载之间的这种层次关系,称为类加载器的双亲委派模型。
工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委托给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该产送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载
参考
- 深入理解Java虚拟机[书籍]