Mr Dk.'s BlogMr Dk.'s Blog
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • 🦆 About Me
  • ⛏️ Technology Stack
  • 🔗 Links
  • 🗒️ About Blog
  • Algorithm
  • C++
  • Compiler
  • Cryptography
  • DevOps
  • Docker
  • Git
  • Java
  • Linux
  • MS Office
  • MySQL
  • Network
  • Operating System
  • Performance
  • PostgreSQL
  • Productivity
  • Solidity
  • Vue.js
  • Web
  • Wireless
  • 🐧 How Linux Works (notes)
  • 🐧 Linux Kernel Comments (notes)
  • 🐧 Linux Kernel Development (notes)
  • 🐤 μc/OS-II Source Code (notes)
  • ☕ Understanding the JVM (notes)
  • ⛸️ Redis Implementation (notes)
  • 🗜️ Understanding Nginx (notes)
  • ⚙️ Netty in Action (notes)
  • ☁️ Spring Microservices (notes)
  • ⚒️ The Annotated STL Sources (notes)
  • ☕ Java Development Kit 8
GitHub
  • ☕ Understanding the JVM
    • Part 2 - 自动内存管理

      • Chapter 2 - Java 内存区域与内存溢出异常
      • Chapter 3.1-3.2 - 概述 && 对象已死
      • Chapter 3.3-3.4 - 垃圾收集算法 && HotSpot 的算法实现细节
      • Chapter 3.5 - 经典垃圾收集器
      • Chapter 3.6 - 低延迟垃圾收集器
    • Part 3 - 虚拟机执行子系统

      • Chapter 6 - 类文件结构
      • Chapter 7 - 虚拟机类加载机制
      • Chapter 8.1-8.2 - 运行时栈帧结构
      • Chapter 8.3 - 函数调用
      • Chapter 8.5 - 基于栈的字节码解释执行引擎
      • Chapter 9.2 - Tomcat: 正统的类加载器架构
    • Part 4 - 程序编译与代码优化

      • Chapter 10 - 前端编译与优化
      • Chapter 11.2 - 即时编译器
      • Chapter 11.3-11.4 - 提前编译器 && 编译器优化技术
    • Part 5 - 高效并发

      • Chapter 12.3 - Java 内存模型
      • Chapter 12.4-12.5 - Java 线程与协程
      • Chapter 13 - 线程安全与锁优化

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 的具体实现自由把握。而对于 初始化 阶段,则有且只有六种情况才会触发:

  1. 遇到 new getstatic putstatic invokestatic 四条字节码指令时,且类型没有初始化;能生成这些字节码的场景:
    • 用 new 关键字实例化对象
    • 读取或设置一个静态字段
    • 调用一个类型的静态方法
  2. 对类型进行反射调用时,且类型没有进行过初始化
  3. 当初始化类时,发现其父类还没有进行过初始化时
  4. 当 JVM 启动时,用户指定一个要执行的主类
  5. 动态语言支持
  6. 当接口定义了默认方法,且该接口的实现类发生了初始化

以上六种场景称为对类型进行 主动引用。


7.3 类加载的过程

7.3.1 加载

JVM 需要完成以下三件事情:

  1. 通过一个类的 全限定名 来获取定义该类的二进制字节流
  2. 将字节流代表的 静态存储结构 转化为方法区的 运行时数据结构
  3. 在内存中生成代表该类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口

并没有指明字节流的来源一定是 Class 文件,因此还可以从压缩包、网络等途径获取。该阶段既可以使用 JVM 内置的 引导类加载器 完成,也可以由用户自定义的类加载器完成:开发人员定义类加载器中字节流的获取方式。

对于数组而言,数组类本身不通过类加载器创建,由 JVM 直接在内存中动态构造,但数组类的元素类型还是要靠类加载器加载。加载阶段结束后,JVM 外部的二进制字节流就按照 JVM 设定的格式存储在方法区中。方法区中的具体存放格式由 JVM 的实现自行定义。

当类型数据妥善放置在方法区中后,JVM 会在 Java Heap 上实例化一个 java.lang.Class 类对象。该对象是程序访问方法区中类型数据的外部接口。

加载 和 链接 阶段的部分动作是交叉进行的,但两个阶段的开始时间依然保持着固定的先后顺序。

7.3.2 验证

确保 Class 文件的字节流中包含的信息符合 JVM 规范中的约束,保证这些信息运行后不会危害虚拟机的安全。Java 语言本身是相对安全的编程语言,对于以下错误:

  • 访问数组边界以外的数据
  • 将对象转换为未实现的类型
  • 跳转到不存在的代码行

编译器将会抛出异常、拒绝编译。但这些功能在字节码的层面上都是可以实现的。验证阶段大致完成四个阶段的检验:

  1. 文件格式验证
    • 验证字节流是否符合 Class 文件格式的规范
    • 是否能被当前版本的 JVM 处理
    • 经过验证后,字节流才可以进入 JVM 的方法区中进行存储
  2. 元数据验证
    • 对字节码描述的信息进行语义分析
    • 对类的 metadata 进行语义校验 (继承关系等)
  3. 字节码验证
    • 通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的
    • 需要对类的方法体进行校验分析,保证运行时不会危害 JVM 的安全
  4. 符号引用验证
    • 发生在 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 以来,一直保持 三层类加载器、双亲委派 的类加载架构。

三个系统提供的类加载器:

  1. 启动类加载器 (Bootstrap Class Loader)
    • 负责加载 ${JAVA_HOME}/lib 目录下的类
    • 只会加载 JVM 能够识别的类
    • 这个加载器不能被 Java 程序直接引用,如果需要委托该加载器处理类加载,直接使用 null 作为参数
  2. 扩展类加载器 (Extension Class Loader)
    • 以 Java 代码的形式实现
    • 负责加载 ${JAVA_HOME}/lib/ext 目录
    • 允许用户将具有通用性的类库放置在该目录下,扩展 Java SE 的功能
    • 开发者可以直接在程序中使用扩展类加载器来加载 Class 文件
  3. 应用程序类加载器 (Application Class Loader)
    • 也称 系统类加载器
    • 负责加载用户类路径 (ClassPath) 下的所有类库
    • 开发者也可以在程序中使用该类加载器

除了顶层的启动类加载器,所有的类加载器都有其父类加载器。父子关系不使用继承的关系来实现,使用组合关系来复用父类加载器的代码。

双亲委派模型的工作过程:一个类加载器收到了类加载请求,首先委派自己的父类加载器完成,每个层次的类加载器都是如此;只有父类加载器无法完成加载请求 (搜索范围内没有找到所需的类) 时,子类加载器才会尝试自己完成加载。Java 中的类随着其类加载器一起具备了带有优先级的层次关系:越基础的类,由越高层的类加载器加载,保证了 Java 程序的稳定运作。

双亲委派模型的实现:

  1. 检查请求加载的类是否已被加载
  2. 若没有加载,则调用父加载器的 loadClass() 方法;若父加载器为空,则使用启动类加载器作为父类加载器
  3. 加入父类加载器加载失败,才调用自己的 findClass() 方法尝试加载

7.5 Java 模块化系统 (Java Platform Module System, JPMS)

这是 JDK 9 中引入的特性,不仅像 JAR 包那样只是简单充当代码的容器,还包括:

  • 依赖其它模块的列表
  • 导出的包列表 (其它模块可以使用的列表)
  • 开放的包列表 (其它模块可反射访问模块的列表)
  • 使用的服务列表
  • 提供服务的实现列表

模块声明了对其它模块的显式依赖。一些异常就不会在运行到类加载时才抛出,而是直接从依赖中得出。可配置的 封装隔离机制 还解决了跨 JAR 文件的 public 类型的可访问性问题:JDK 9 中的 public 关键字不再意味着程序的所有代码都可以随意访问它。

模块提供了更精细的访问控制,必须明确声明哪一些 public 类型可以被其它哪一些模块访问。

Edit this page on GitHub
Prev
Chapter 6 - 类文件结构
Next
Chapter 8.1-8.2 - 运行时栈帧结构