03-JVM类加载机制你只多少?
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如图:
image
加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始, 这是为了支持Java语言的运行时绑定特性(也称为动态绑定或晚期绑定)。
一.类的加载时机
思考:JVM在什么情况下会加载一个类呢?
通过以上的类加载流程,我们可以得知第一个环节就是加载一个类,因此当我们在IDEA中或直接运行某一个类的时候(比如First.java),其实是启动了JVM进程,然后JVM会通过类加载器将这个类的字节码(First.class)加载到内存,然后调用main方法开始执行。如果main方法中的代码是:
public class First {
public static void main(String[] args) {
//创建Second这个类的实例
Second second = new Second();
}
}
JVM这个时候会先检查内存中是否有该类的对象,如果没有会触发类加载器加载磁盘中的Second.class字节码到内存中,如下图:
二.类的加载阶段
“加载”是“类加载”(Class Loading)过程的一个阶段。在加载阶段,虚拟机需要完成以下3件事情:
(PS:方法区理解为JVM内存中的一块区域)
1)通过一个类的全限定名来获取定义此类的二进制字节流。
- 从zip压缩包中读取(jar、war、EAR等格式)
- 从网络中获取,比如Web Applet
- 运行的时候动态生产---动态代理技术
- 由其他文件生产,比如JSP应用
- 从数据库中读取(少见)
- 可以从加密文件中获取
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
- _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
- _super 即父类
- _fields 即成员变量
- _methods 即方法
- _constants 即常量池
- _class_loader 即类加载器
- _vtable 虚方法表
- _itable 接口方法表
- 如果这个类还有父类没有加载,先加载父类
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区之中 了,(instanceKlass )
方法区中的数据存储格式完全由虚拟机实现自行定义,《Java虚拟机规范》未规定此区域的具体 数据结构。
类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象,
这个对象将作为程序访问方法区中的类型数据的外部接口。 加载阶段与连接阶段的部分动作(如一部分字节码文件格式验证动作)
是交叉进行的,加载阶段 尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的一部 分,
这两个阶段的开始时间仍然保持着固定的先后顺序。
image
三.类的连接阶段
连接阶段包括:验证、准备、初始化,对于这三个阶段没有太大的必要去深入研究里面的细节,这里的细节很多很繁琐,对于大部分同学来说重点理解其中的一些核心概念即可。
3.1验证阶段
验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚 拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
主要包括四种验证,文件格式验证(魔数CAFEBABE),元数据验证,字节码验证,符号引用验证。
简单说就是我们的【.class】文件是否符合JVM规范,是否有被篡改,否则JVM是没法执行该字节码文件的。
[图片上传失败...(image-d69155-1628075278654)]_20210716174354.png)
3.2准备阶段
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段
-
static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
(换句话理解就是:1.7之前存储于方法区,1.7之后存储于堆内存中)
-
static 变量分配空间和赋值是两个步骤,分配空间(内存分配)在准备阶段完成,赋值在初始化阶段完成
关于准备阶段,还有两个容易产生混淆的概念需要着重强调,首先是这时候进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。
其次是这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value = 123;
那变量value在准备阶段过后的初始值为0而不是123,因为这时尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作要到类的初始化阶段才会被执行。
-
如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
上面提到在“通常情况”下初始值是零值,那言外之意是相对的会有某些“特殊情况”:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量值就会被初始化为ConstantValue属性所指定的初始值,假设上面类变量value的定义修改为:
public static final int value = 123;
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。
-
如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
测试代码:
public class Second {
static int a;
static int b = 10;
static final int c = 20;
static final String d = "hello";
User user = new User();
}
反编译后的代码:
可见c和d均在准备阶段即完成赋值:
imagea、b、user在初始化阶段完成赋值:
image3.3解析阶段
主要将常量池中的符号引用替换为直接引用的过程。
常量池的概念(提前了解)
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)ff的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:
-
被模块导出或者开放的包(Package)
-
类和接口的全限定名(Fully Qualified Name)
-
字段的名称和描述符(Descriptor)
-
方法的名称和描述符
-
方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
-
动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
Java代码在进行Javac编译的时候,在虚拟机加载Class 文件的时候进行动态连接。在Class文件中不会保存各个方法、字段最终 在内存中的布局信息,这些字段、方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址,也就无法直接被虚拟机使用的。当虚拟机做类加载时,将会从常量池获得对应的符号 引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
-
符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定是已经加载到虚拟机内存当中的内容。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在《Java虚拟机规范》的Class文件格式中。
-
直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局直接相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。
3.4小结
这三个阶段中,大家最应该关心的核心是:准备阶段
这个阶段是给加载进来的类进行空间的分配,以及static静态变量的空间分配,并且给与初始化值。
image