本准备参考 Java 虚拟机规范文档写一篇完整的类加载文档,求大求全,最后发现成了官方文档类加载的翻译。于是重新整理了这一篇简单的记录。
1. 类加载过程
Java 虚拟机规范中说明,类加载分为 加载、链接、初始化 三个步骤。
1.1 加载
加载分为 创建和加载,其中针对类分为数组和非数组。
数组由 Java 虚拟机创建。
加载完成以下事情:
- 通过类名获取定义此类的二进制流
- 将字节流内容转化为方法区的运行时数据结构
- 生成代表此类的 Class 对象,作为方法区此类的访问入口
在 Java 运行时添加
-verbose:class
或者-XX:+TraceClassLoading
可以查看类加载过程。
1.2 链接
链接分为验证和准备两个步骤,包含父类和实现的接口,如果是数组,还包含了元素的类型。此阶段解析符号引用是可选的。Java 虚拟机可以选择在使用时解析符号引用,也可以在验证完成之后直接全部解析。这意味着解析可能出现在初始化之后。解析时的异常必须是在使用对应符号时产生。由于链接过程会消耗内存,因此可能产生 OutOfMemoryError。
链接在加载完成之后进行。
在初始化之前会完成验证和准备工作。
链接过程中出现的错误抛出时机是在程序的操作必须进行链接的时候。
验证保证 Class 的字节流信息符合虚拟机规范,包含:文件格式校验,元数据验证,字节码验证,符号引用验证
准备阶段为类变量分配内存并赋初值(0等),如果类变量定义为 final 还会直接赋值(常量值)。
解析是将符号引用解析为直接引用。符号引用是对应描述,直接引用是内存地址。
1.3 初始化
类和接口的初始化在以下场景触发:
- 执行类的以下 Java 虚拟机指令
new
,getstatic
,putstatic
,invokestatic
- java.lang.invoke.MethodHandle 解析的结果是 2 (REF_getStatic), 4 (REF_putStatic), 6 (REF_invokeStatic), or 8 (REF_newInvokeSpecial)
- 通过反射进行调用
- 字类初始化会触发父类初始化
- 如果接口包含非抽象,非静态的方法(默认方法),接口的直接或间接实现类会触发接口初始化
- Java 虚拟机启动的主类
初始化之前会执行链接操作(验证、准备以及可选的解析)。
2. 类加载机制
2.1 双亲委托
这个名字比较坑,实际上类加载会委派给父类加载器。在父类加载器无法处理的情况下再由自己进行处理。
Java虚拟机规范文档中说明了一个好的类加载器实现保证以下特性:
- 针对相同的名称,应该返回相同的 Class 对象
- 如果类加载器委派另一个类加载器加载类,那么此类的父类、实现的接口、成员变量、方法和构造方法的参数以及返回值等类型,这两个类加载器返回同一个 Class 对象
- 如果一个自定义类加载器可以预加载类和接口的二进制表示,那么加载错误不应该在预加载时候抛出
那么什么情况下存在不好的实现呢?
- Java 1.2 之前对类加载器还没有这些要求。
- JNDI(Java Naming and Directory Interface) 中 SPI(Service Provider Interface) 由于框架完成时具体实现是动态引入的,所以类加载器不是自下而上的。
- 热部署,典型的 OSGI(Open Service Gateway Initiative),动态增删模块。