class 类加载过程(JDK 8)

读《深入理解 Java 虚拟机》第三版,周志明著,笔记。
官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-5.html

这里我采用官方给出的三个阶段来说明:加载(Loading)、连接(Linking)、初始化(Initializing)。

一、加载(Loading)

Java 虚拟机需要完成三件事情:

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

这里没有规定二进制字节流必须来源于某个 class 文件。所以二进制字节流的来源可以是:

  • ZIP压缩文件。
  • 从网络获取。
  • 运行时计算生成,动态代理产生。
  • 其他文件,如 jsp。
  • 数据库。
  • 加密后的class文件。
  • 等等。

二、连接(Linking)

连接过程有三个:验证(Verification)、准备(Preparation)、解析(Resolution)。

2.1、验证(Verification)

目的是确保 Class 文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。

大致可以分为以下四个步骤。

2.1.1 文件格式验证

基于二进制字节流(后面验证都是基于方法区的数据),是否符合 Class 文件格式。

主要有:

  • 是否以魔数 0xCAFEBABE 开头。
  • 主、次版本号是否在当前 Java 虚拟机接受范围之内。
  • 常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)。
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info 型常量中是否有不符合 UTF-8 编码的数据。
  • class 文件中各个部分及文件本身是否有被删除的或附近的其他信息。
  • 等等。

2.1.2 元数据验证

对类的元数据信息经行语义校验。

主要有:

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

2.1.3 字节码验证

通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。主要针对类的方法体(Class 文件中的 Code 属性)经行校验分析。

主要有:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
  • 保证任何跳转指令都不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换总是有效的。
  • 等等。

JDK 6 以后 javac 编译器在 Code 属性中添加了 “StackMapTable” 属性,描述了方法体所有的基本块(Basic Block,指按照控制流拆分的代码块)开始时本地变量表和操作数栈应有的状态,将字节码验证的类型推导转变为类型检查,节省校验时间。

2.1.4 符号引用验证

发生在解析(Resolution)阶段,虚拟机将符号引用转变为直接引用时。目的是保证解析行为能正常进行。

主要有:

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

-Xverify:none 参数可以关闭大部分的类校验措施,来缩短虚拟机加载类的时间。

2.2、准备(Preparation)

正式为类中定义的变量(即静态变量,被 static 修饰的变量)分配内存并设置类变量初始值

描述 内存分配 初始化
类变量 被 static 修饰的变量 类加载时准备阶段 类构造器()
实例变量 没有被 static 修饰的变量 类的对象被分配到Java堆时 构造函数

基本数据类型的零值(初始值):

数据类型 零值
int 0
long 0L
short (short) 0
char \u0000
byte (byte) 0
boolean false
float 0.0f
double 0.0d
reference null

如果被 final 修饰了,会附加 ConstantValue 属性,那么准备阶段会被赋值。

2.3、解析(Resolution)

将常量池内的符号引用替换为直接引用的过程。

符号引用(Symbolic References) 直接引用(Direct References)
一组描述引用目标的任意符号 直接指向目标的指针、相对偏移量、句柄
与虚拟机实现的内存布局无关 与虚拟机实现的内存布局直接相关
《Java虚拟机规范》定义 虚拟机实现内定义

《Java虚拟机规范》没有规定具体的实现解析的时间,只是要求执行,anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic,invokevirtual, ldc, ldc_w, multianewarray, new, putfield, putstatic(这里选用JDK8的ldc2_w不在内,jdk11开始才有)16 种之前将符号引用解析成直接引用。

对于除了 invokedynamic以外指令,虚拟机在第一次成功解析以后会缓存,后面不会再解析了。invokedynamic 则是每次都会动态去解析。

  • 类或接口解析

  • 字段解析

  • 方法解析

  • 接口方法解析

三、初始化(Initializing)

初始化阶段就是执行类构造器 ()方法的过程。

  • ()方法:是由编译器自动收集类中的所有类变量的赋值动作和静态语句块合并构成。
  • ()方法:与类的构造函数不一样,不能显式调用。
  • ()方法:并不是必须的,当时没有类变量赋值操作和静态语句时不会有。
  • ()方法:执行时不用先执行父类的()方法。
  • ()方法:在多线程环境下必须保证被正确加锁执行。当一个线程执行时,其他线程需要阻塞等待。

《Java虚拟机规范》规定了有且只有 6 种情况必须对类执行 “ 初始化 ” :

  1. 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,类没有被初始化。
  2. 使用 java.lang.reflect 包的方法对类型进行反射调用时。
  3. 当初始化类时,父类没有初始化,父类初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),初始化主类。
  5. 当使用 JDK 7 开始的动态语言支持时,如果一个 java.lang.invoke.MethodeHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial 四种类型的方法句柄,对应的类没有进行初始化时。
  6. 当调用 JDK 8 开始的默认方法(被default修饰的接口方法)的接口没有被初始化时。

经过类加载后,就可以被正常的实例化使用,或者调用了。

你可能感兴趣的:(JVM)