灰气球

灰气球

JVM 类加载的过程

133
2021-04-28

《深入理解Java虚拟机》- 第三版
《深入浅出Java虚拟机》- 李国

前言

Java 虚拟机中类加载的全过程,即加载、验证、准备、解析和初始化, 这五个阶段所执行的具体动作

加载

“加载”( Loading)阶段是整个“ 类加载”( Class Loading)过程中的一个阶段。在加载阶段,Java 虚拟机需要完成以下三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。《Java 虚拟 机规范》对这三点要求其实并不是特别具体,留给虚拟机实现与 Java 应用的灵活度都是相当大的。
加载阶段结束后, Java 虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中了,方法区中的 数据存储格式完全由虚拟机实现自行定义,《 Java 虚拟机规范》未规定此区域的具体数据结构。类型数据妥善安置在方法区之后,会在 Java 堆内存中实例化一个 java.lang.Class 类的对象, 这个对象将作为程序访问方法区中的 类型数据的外部接口。
加载阶段与连接阶段的部分动作( 如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部分,这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

验证是连接阶段的第一步,这一阶段的目的是确保 Class 文件的字节流中包含的信息符合《 Java 虚拟机规范》 的全部约束要求, 保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
Java 语言本身是相对安全的编程语言( 起码对于 C/ C++ 来说是相对安全的),使用纯粹的 Java 代码无法做到 诸如访问数组边界以外的数据、将一个对象转型为它并未实现的类型、跳转到不存在的代码行之类的事情,如果 尝试这样去做了,编译器会毫不留情地抛出异常、拒绝编译。但前面也曾说过,Class 文件并不一定只能由 Java 源码编译而来,它可以使用包括靠键盘 0 和 1 直接在二进制编辑器中敲出 Class 文件在内的任何途径产生。上述 Java 代码无法做到的事情在字节码层面上都是可以实现的,至少语义上是可以表达出来的。Java 虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有错误或有恶意企图的字节码流而导致整个系统受攻击 甚至崩溃,所以验证字节码是 Java 虚拟机保护自身的一项必要措施。
验证阶段是非常重要的,这个阶段是否严谨,直接决定了 Java 虚拟机是否能承受恶意代码的攻击,从代码量和 耗费的执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重。从整体上看,验证阶段大致上会完成下面四个阶段 的检验动作:文件格式验证、 元数据验证、字节码验证和符号引用验证。

  1. 文件格式验证
    第一阶段要验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
  2. 元数据验证
    第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合《 Java 语言规范》 的要求。
  3. 字节码验证
    第三 阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型校验完毕以后,这阶段就要对类的方法体( Class 文件中的 Code 属性)进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
  4. 符号引用验证

最后一个阶段的校验行为发生在虚拟机将符号引用转化为直接引用 [3] 的时候,这个转化动作将在连接的第三 阶段——解析阶段中发生。符号引用验证可以看作是对类自身以外( 常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。

准备

准备阶段是正式为类中定义的变量( 即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值的阶段,从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域,在 JDK 7 及之前,HotSpot 使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;而在 JDK 8 及之后, 类变量则会随着 Class 对象一起存放在 Java 堆 中。
关于准备阶段,还有两个容易产生混淆的概念笔者需要着重强调,首先是这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。

解析

解析阶段是 Java 虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 符号引用( Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面 量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是 已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《 Java 虚拟机规范》的 Class 文件格式中。
  • 直接引用( Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出 来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。

初始化

类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由 Java 虚拟机来主导控制。直到初始化阶段, Java 虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。
进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的 主观计划去初始化类变量和其他资源。我们也可以从另外一种更直接的形式来表达:初始化阶段就是执行类构造器< clinit>() 方法的过程。