JVM 中类加载的链接与初始化

1. 前言

对于类加载子系统,前边的课程已经对加载(Loading)这一步做了详细的讲解,本节主要对类加载子系统加载步骤中的链接与初始化进行讲解。本节主要知识点如下:

  • 链接(Linking)步骤更加详细的模块划分:验证,准备和解析,为本节基础知识点;
  • 掌握在链接(Linking)步骤中的第一步验证的详细验证内容,为本节重点内容之一;
  • 掌握在链接(Linking)步骤中的第二步准备的准备内容,为本节重点内容之一;
  • 掌握在链接(Linking)步骤中的第三步解析的具体解析内容,为本节重点内容之一;
  • 掌握初始化(Init)步骤中的规则以及实例初始化顺序,为本节重点内容之一。

通篇皆为重点内容,本节知识也会为类加载子系统部分画上一个完美的句号,一定要认真对待。

2. 类加载子系统知识回顾

我们在JVM 总体架构的讲解过程中,提到过类加载子系统的工作流程分为三步:加载->链接->初始化。如下图所示:

图片描述

本节我们所讨论的内容都是围绕第二步“链接(Linking)” 和第三步“初始化(Init)”进行的。
我们将链接(Linking)这一步,再进行下细致的模块划分,如下图所示:

图片描述

从上图中我们可看到,链接(Linking)这一步,里边包含了三个更加细致的步骤,分别为验证(verify)准备(prepare)解析(resolve)。后文我们会对这三个步骤进行讲解。

3. 链接-验证(verify)

定义:验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并不会危害虚拟机的自身安全。

验证过程的主要验证信息:验证过程中,主要对三种类型的数据进行验证,分别是“元数据验证,字节码验证和符号引用验证”。具体内容请看下边的讲解。

元数据验证

  • 验证这个类是否有父类(除了 java.lang.Object 之外,所有类都应当有父类);
  • 验证这个类是否继承了不允许被继承的类(被 final 修饰的类);
  • 如果这个类不是抽象类,验证该类是否实现了其父类或接口之中所要求实现的所有方法;
  • 验证类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等等)。

字节码验证:字节码验证主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会产生危害虚拟机安全的事件,例如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。例如不会出现类似这样的情况:在操作数栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中;
  • 保证跳转指令不会跳转到方法体以外的字节码指令上;
  • 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系、完全不相干的一个数据类型,则是危险不合法的。

符号引用验证:符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,通常需要校验以下内容:

  • 符号引用中通过字符串描述的全限定名是否能够找到对应的类;
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;
  • 符号引用中的类、字段、方法的访问性(private、default、protected、public)是否可被当前类访问。

4. 链接-准备(prepare)

定义:准备阶段是正式为类变量分配内存并设置类变量默认值(通常情况下是数据类型的零值)的阶段,这些变量所使用的内存都将在方法区中进行分配。这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。

Tips:准备阶段是设置类变量的默认值,不同类型的类变量的默认值是不同的。变量默认值的对照表请参看下表:

变量类型

默认值

int

0

long

0L

short

0

char

‘u0000’

byte

0

boolean

false

folat

0.0f

double

0.0d

reference

null

5. 链接-解析(resolve)

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

Tips:定义中又引出了两个新的概念:符号引用和直接引用。想要理解解析,必须要先搞明白什么是符号引用和直接引用。

符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。

直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标一定是已经存在于内存中。

解析过程具体的解析内容:解析过程中,主要对如下4种类型的数据进行验证:

  • 类或接口的解析;
  • 字段解析;
  • 类方法解析;
  • 接口方法解析。

6. 初始化

定义:进行准备阶段时,变量已经赋过一次系统要求的初始零值,而在初始化阶段,则会根据程序员通过程序编码制定的主观计划去初始化类变量和其他资源。

类的初始化阶段是类加载过程的最后一个步骤,之前介绍的几个类加载的动作里,除了在加载阶段用户应用程序可以通过自定义类加载器的方式局部参与外,其余动作都完全由Java虚拟机来主导控制。直到初始化阶段,Java虚拟机才真正开始执行类中编写的 Java 程序代码,将主导权移交给应用程序。

实例的初始化顺序:在进行初始化时,实例变量的初始化顺序如下图所示:

图片描述

实例的初始化顺序是非常重要的知识点,在面试过程中也经常涉及到这个知识点,上图的加载顺序需要重点掌握。

7. 小结

到目前为止,类加载器子系统就全部讲解完成了。我们学习了类的加载,三种类加载器,双亲委派模型以及本节所讲述的链接与初始化,其中对链接有细分了三个步骤进行了讲解。

类加载器子系统是非常重要的 JVM 模块,需要用心学习,对于一些概念性知识要增强理解,原理性知识要深入思索。后续我们会继续讲解 JVM 的其他重要模块。