一、前言
前面我们了解了字节码文件的大致组成部分,那么 JVM
是如何加载 .class
字节码文件的?加载到.class
字节码文件后又做了哪些事情呢?
二、类加载子系统初步认识
首先类加载子系统作为虚拟机和外界的一个对接口,主要负责以下几点:
- 负责从文件或者网络加载
Class
字节流 - 读取字节码中的信息,存入 JVM 内存中 (方法区)
- 对字节流进行规范化校验
三、类加载器子系统加载过程
如下图中所示,.class
文件最先由类加载器子系统进行处理,而类加载器子系统进行一个类的加载的时候内部大致可分为三个阶段,加载阶段 -> 链接阶段 -> 初始化阶段。见下图
3.1 加载阶段 Loading
这里我就不摘抄书中内容了,用比较通俗的语言描述,这里加载其实就是读取字节流内容到内存中,通过类的全限定名来进行定位,读取到内容后,将所有的静态结构转化为运行时数据结构,然后存储的方法区中,然后生成一个这个类的 java.lang.Class 对象,放入堆中,作为在方法区中这个类的访问入口。注意这个阶段只负责读和存,不做任何验证处理。加载的过程中,必然会触发父类的加载。
补充说明:Class 实例是如何被创建的。
- new 实例化
A a = new A();
- 反射
Class clzA = Class.forName("com.xxx.xxx.A");
- 子类加载时作为父类同时加载
- JVM 启动时,包含 main 方法的主类
- 1.7 的动态类型语言支持
3.2 链接阶段
- 验证 Verify
- 文件格式验证:验证魔数、版本、常量池这些格式相关的数据
- 元数据验证:这个类是否有父类,以及这个类的父类是不是被 final 修饰,不允许被继承等。主要针对元数据语义方面的校验
- 字节码校验:这个阶段最为复杂,主要通过数据流分析和控制流分析,确定语义合法,符合逻辑。针对类的方法体(Class 文件中的 Code 属性)进行校验分析,保证方法运行时不出现危害虚拟机安全的行为。
- 符号引用验证:这个在整个链接阶段中的最后一个阶段,解析阶段中将符号引用转化为直接引用时发生,比如:全限定类名是否能找到该类。
验证和加载并不是线性的关系,并不是先将流信息全部加载完,再去逐行验证,而是交替进行,可以理解为一边读一边校验。注意这里验证的只是针对流文件的内容,静态数据校验,与运行时环境无关,所以目标 Class 对象不一定已经被加载到内存中,符号引用是由字节码规定的。
验证阶段非常重要,但是没必要每次都进行验证,只要通过了之后对程序运行期没有任何影响了。如果程序运行全部代码都已经反复使用和验证过,生成环境的实施阶段可以考虑使用 -Verify:none 参数来关闭大部分类验证措施,以缩短虚拟机类的加载时间。
- 准备 Prepare
- 为类中定义的变量分配内存并设置类变量初始值(注意:这里指类变量,static修饰的变量),这里的初始值 “通常情况” 下是数据类型的零值,比如
public static int value = 123;
, 那么准备阶段初始值为0
而不是123
。参考下表。
- 为类中定义的变量分配内存并设置类变量初始值(注意:这里指类变量,static修饰的变量),这里的初始值 “通常情况” 下是数据类型的零值,比如
- **解析 Resolve **
- 解析是将常量池内的符号引用替换为直接引用的过程,符号引用:一些字面量;直接引用:可以直接指向目标的指针、相对偏移量或者是间接定位到目标的句柄。
- 类\接口解析、字段解析、方法解析、接口方法解析
3.3 初始化阶段
执行类构造器 <clinit>()
方法。此方法是执行过程中,由编译器自动收集类中的所有变量的赋值动作和静态代码块(static{}
块)中的语句合并产生的。编译器收集的顺序是语句在源文件中出现的顺序决定的,所以静态代码块中只能访问到定义在之前的变量,定义在后面的变量,只能进行赋值,不能访问。
执行过程的本质:
- 对静态变量赋值以及执行静态代码块
- 子类初始化过程会优先执行父类的
<clinit>()
- 没有静态变量及静态代码块(
static{}
) 就不会产生<clinit>()
- 设置启动参数
-XX: +TraceClassLoading
查看类加载过程 <clinit>()
方法会默认增加同步索确保只执行一次