Chapter 7 - 虚拟机类加载机制
Created by : Mr Dk.
2020 / 01 / 29 22:12 🧨🧧
Ningbo, Zhejiang, China
7.1 概述
Class 文件中描述的各类信息,最终都需要加载到 JVM 中才能被运行和使用。类加载:JVM 把 Class 文件加载到内存中,并对数据进行校验、转换、解析和初始化,最终形成可以被 JVM 使用的 Java 类型。类型的加载、链接、初始化都在程序 运行时 完成,因此会有一定的性能开销。
7.2 类加载的时机
一个类型从被加载到 JVM 内存中开始,到卸载出内存,包含七个阶段:
- 加载 (Loading)
- 验证 (Verification)
- 准备 (Preparation)
- 解析 (Resolution)
- 初始化 (Initialization)
- 使用 (Using)
- 卸载 (Unloading)
其中,验证、准备、解析三个阶段统称为链接 (Linking)。JVM 的规范并没有规定在什么情况下需要开始第一阶段的加载,由 JVM 的具体实现自由把握。而对于 初始化 阶段,则有且只有六种情况才会触发:
- 遇到
new
getstatic
putstatic
invokestatic
四条字节码指令时,且类型没有初始化;能生成这些字节码的场景:- 用
new
关键字实例化对象 - 读取或设置一个静态字段
- 调用一个类型的静态方法
- 用
- 对类型进行反射调用时,且类型没有进行过初始化
- 当初始化类时,发现其父类还没有进行过初始化时
- 当 JVM 启动时,用户指定一个要执行的主类
- 动态语言支持
- 当接口定义了默认方法,且该接口的实现类发生了初始化
以上六种场景称为对类型进行 主动引用。
7.3 类加载的过程
7.3.1 加载
JVM 需要完成以下三件事情:
- 通过一个类的 全限定名 来获取定义该类的二进制字节流
- 将字节流代表的 静态存储结构 转化为方法区的 运行时数据结构
- 在内存中生成代表该类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口
并没有指明字节流的来源一定是 Class 文件,因此还可以从压缩包、网络等途径获取。该阶段既可以使用 JVM 内置的 引导类加载器 完成,也可以由用户自定义的类加载器完成:开发人员定义类加载器中字节流的获取方式。
对于数组而言,数组类本身不通过类加载器创建,由 JVM 直接在内存中动态构造,但数组类的元素类型还是要靠类加载器加载。加载阶段结束后,JVM 外部的二进制字节流就按照 JVM 设定的格式存储在方法区中。方法区中的具体存放格式由 JVM 的实现自行定义。
当类型数据妥善放置在方法区中后,JVM 会在 Java Heap 上实例化一个 java.lang.Class
类对象。该对象是程序访问方法区中类型数据的外部接口。
加载 和 链接 阶段的部分动作是交叉进行的,但两个阶段的开始时间依然保持着固定的先后顺序。
7.3.2 验证
确保 Class 文件的字节流中包含的信息符合 JVM 规范中的约束,保证这些信息运行后不会危害虚拟机的安全。Java 语言本身是相对安全的编程语言,对于以下错误:
- 访问数组边界以外的数据
- 将对象转换为未实现的类型
- 跳转到不存在的代码行
编译器将会抛出异常、拒绝编译。但这些功能在字节码的层面上都是可以实现的。验证阶段大致完成四个阶段的检验:
- 文件格式验证
- 验证字节流是否符合 Class 文件格式的规范
- 是否能被当前版本的 JVM 处理
- 经过验证后,字节流才可以进入 JVM 的方法区中进行存储
- 元数据验证
- 对字节码描述的信息进行语义分析
- 对类的 metadata 进行语义校验 (继承关系等)
- 字节码验证
- 通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
- 需要对类的方法体进行校验分析,保证运行时不会危害 JVM 的安全
- 符号引用验证
- 发生在 JVM 的 符号引用 转化为 直接引用 时
- 检验该类是否缺少或禁止访问它依赖的外部资源
- 确保解析行为能正常执行
7.3.3 准备
正式为 类中定义的变量 (静态变量) 分配内存并设置初始值
- 仅包括类变量,不包括实例变量
- 初始值通常为 0,哪怕代码中定义了初始值,也会在初始化阶段才进行赋值
public static int value = 123;
而如果类字段属性表中存在 ConstantValue 属性
那么在该阶段变量值就会被初始化为指定的值,比如:
public static final int value = 123;
7.3.4 解析
是 JVM 将常量池内的符号引用替换为直接引用的过程
- 符号引用 (Symbolic References)
- 与 JVM 实现的内存布局无关
- 引用的目标不一定是已经加载到 JVM 中的内容
- 明确定义在 Class 文件格式中
- 直接引用 (Direct References)
- 是直接可以指向目标的指针、偏移量或能间接定位到目标的句柄
- 与 JVM 实现的内存布局直接相关
- 引用目标必定已经在 JVM 的内存中
JVM 实现可以根据需要自行判断开始该阶段的时机。对同一个符号引用进行多次解析很常见:
- JVM 实现可以将第一次解析的结果进行缓存
- JVM 需要保证在同一个实体中,如果一个符号引用之前已经被成功解析,那么后续的解析就应当一直能够成功
7.3.5 初始化
类初始化是类加载过程的最后一个步骤。直到该阶段,JVM 才真正开始执行类中编写的 Java 代码。该阶段就是执行类构造器 <clinit>()
函数的过程。这不是在 Java 代码中编写的函数,是 Javac 编译器的自动生成物。
- 编译器自动收集类中的所有类变量的赋值动作和静态语句块,合并产生
- 不需要显式调用父类构造器,保证在子类
<clinit>()
执行前,父类的<clinit>()
方法已经执行完毕 - 不是必须的 - (没有静态语句块,没有类变量赋值)
- JVM 需要保证
<clinit>()
在多线程环境中被正确同步,只能由一个线程执行
7.4 类加载器
JVM 设计团队特意把 通过一个类的全限定名来获取描述该类的二进制流 的动作放到 JVM 的外部实现。应用程序可以自己决定如何获取所需的类,实现这个动作的代码就是 类加载器 (Class Loader)。
7.4.1 类与类加载器
对于任意一个类,由 加载它的类加载器 和 类本身 一起确定其在 JVM 中的唯一性。每个类加载器都有独立的类名称空间。比较两个类是否相等,只有在两个类是由同一个类加载器载入 JVM 的前提下才有意义,哪怕它们来自同一个 Class 文件。
7.4.2 双亲委派模型
从 JVM 的角度来看,只有两种不同的类加载器:
- 启动类加载器 (Bootstrap ClassLoader)
- 由 C++ 实现,是 JVM 自身的一部分
- 其它的所有类加载器
- 由 Java 实现,独立存在于 JVM 外部
- 全部继承自
java.lang.ClassLoader
Java 自 JDK 1.2 以来,一直保持 三层类加载器、双亲委派 的类加载架构。
三个系统提供的类加载器:
- 启动类加载器 (Bootstrap Class Loader)
- 负责加载
${JAVA_HOME}/lib
目录下的类 - 只会加载 JVM 能够识别的类
- 这个加载器不能被 Java 程序直接引用,如果需要委托该加载器处理类加载,直接使用
null
作为参数
- 负责加载
- 扩展类加载器 (Extension Class Loader)
- 以 Java 代码的形式实现
- 负责加载
${JAVA_HOME}/lib/ext
目录 - 允许用户将具有通用性的类库放置在该目录下,扩展 Java SE 的功能
- 开发者可以直接在程序中使用扩展类加载器来加载 Class 文件
- 应用程序类加载器 (Application Class Loader)
- 也称 系统类加载器
- 负责加载用户类路径 (ClassPath) 下的所有类库
- 开发者也可以在程序中使用该类加载器
除了顶层的启动类加载器,所有的类加载器都有其父类加载器。父子关系不使用继承的关系来实现,使用组合关系来复用父类加载器的代码。
双亲委派模型的工作过程:一个类加载器收到了类加载请求,首先委派自己的父类加载器完成,每个层次的类加载器都是如此;只有父类加载器无法完成加载请求 (搜索范围内没有找到所需的类) 时,子类加载器才会尝试自己完成加载。Java 中的类随着其类加载器一起具备了带有优先级的层次关系:越基础的类,由越高层的类加载器加载,保证了 Java 程序的稳定运作。
双亲委派模型的实现:
- 检查请求加载的类是否已被加载
- 若没有加载,则调用父加载器的
loadClass()
方法;若父加载器为空,则使用启动类加载器作为父类加载器 - 加入父类加载器加载失败,才调用自己的
findClass()
方法尝试加载
7.5 Java 模块化系统 (Java Platform Module System, JPMS)
这是 JDK 9 中引入的特性,不仅像 JAR 包那样只是简单充当代码的容器,还包括:
- 依赖其它模块的列表
- 导出的包列表 (其它模块可以使用的列表)
- 开放的包列表 (其它模块可反射访问模块的列表)
- 使用的服务列表
- 提供服务的实现列表
模块声明了对其它模块的显式依赖。一些异常就不会在运行到类加载时才抛出,而是直接从依赖中得出。可配置的 封装隔离机制 还解决了跨 JAR 文件的 public
类型的可访问性问题:JDK 9 中的 public
关键字不再意味着程序的所有代码都可以随意访问它。
模块提供了更精细的访问控制,必须明确声明哪一些 public
类型可以被其它哪一些模块访问。