Follow me on GitHub

类加载(四):五阶段详解

类加载机制可分为 3 个大阶段:加载、连接、初始化,连接又由 3 个小阶段组成:验证、准备、解析。

加载

加载阶段,JVM 规范要求:

  1. 根据类的 全限定名,获取定义该类的二进制字节流;
  2. 将该字节流中的 静态数据结构 转化为 方法区 中的 运行时数据结构
  3. 内存 中生成代表该类的 java.lang.Class 实例,作为 方法区 中该类各种数据的访问入口;

以上要求非常笼统,JVM 实现可非常灵活处理,比如类的二进制字节流来源可以是:

  • 压缩包:zip、jar、war 等
  • 网络:applet
  • 动态生成:动态代理
  • 其他文件编译得到:JSP
  • 数据库

根据全限定名获取定义类的二进制字节流是整个类加载阶段中,唯一 可被自由定制的,可通过自定义 类加载器 影响该步骤。

第 3 步生成的 Class 实例是对象,但不强制存储在 中,比如 HotSpot 将该对象存储在 方法区,由虚拟机实现自行决定。

连接

连接并非要等到加载完成后才启动,两者实际交叉执行。

验证

验证阶段:保证加载的二进制字节流符合当前虚拟机实现的要求,不会危害 JVM 的安全。

Java 语言本身比较安全:

  • 无法越界访问数组外数据;
  • 无法将对象转型为其未实现的类型;
  • 无法跳转到不存在的代码行;

由 Javac 编译生成的字节码流(class 文件)也是安全的,但 JVM 执行的字节码来源多种多样,比如可以动态生成、网络加载等,从字节码层次看,上面的不安全操作都可以实现,因此 JVM 必须对字节码流进行验。

验证阶段 不是必须 的,如果能保证执行的字节码是安全的,则可关闭大部分验证工作,加快类加载速度。

验证的主要内容:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

其中符号引用验证在“解析”阶段执行,将符号引用转换为直接引用时,需要使用类外数据,验证内容:

  • 符号引用中的 全限定名 是否能找到对应类;
  • 该类是否存在对应字段、方法;
  • 该类的方法、字段是否可被当前类访问;

准备

准备阶段:为类的 静态字段 分配内存,并设置初始值。

类的静态字段存储在 方法区,且这里的初始值“通常”指字段类型的 零值,如:

1
private static int v = 666;

准备阶段过后,v 的值为 0,而非 666。

v = 666 将被 Javac 编译到 <clinit> 方法中,并在“初始化”阶段执行。

但若 vstatic final 修饰,则该字段为常量,准备阶段即将其初始化为 666。

解析

解析阶段:将 常量池 中的符号引用替换为直接引用。

JVM 规定要求,遇到如下 操作符号引用 的字节码指令之前,必须解析其符号引用:

  • anewarraycheckcast、…

具体执行时机又 JVM 实现自行决定,既可在“加载”阶段就解析,也可在实际使用某个符号引用时再解析。

初始化

初始化阶段:执行 <clinit> 方法。

前面的加载、连接阶段,与 Java 源码 毫无关联,初始化阶段才开始执行 Java 源码中的指令。

<clinit> 方法是 Javac 编译器自动生成的,包含两部分:

  • static 字段的赋值语句;
  • static 块;

它们在 <clinit> 中的顺序与其在 Java 源码中的 顺序相同

<clinit> 中不会显式调用父类的 <clinit> 方法,因为 JVM 会保证父类的 <clinit> 一定 先于 子类的 <clinit> 执行,因此父类的 static 字段赋值、static 块都会先于子类的执行。

JVM 保证 <clinit>线程安全(锁)的,因此同一个类只会被初始化一次。