JVM学习笔记—类加载器子系统
0. 类加载机制
- 类加载机制是指我们将类的字节码文件所包含的数据读入内存,同时我们会生成数据的访问入口的一种特殊机制。那么我们可以得知,类加载的最终产品是数据访问入口。
1. 类装载子系统
- 类加载子系统负责从文件系统或是网络中加载.class文件,class文件在文件开头有特定的文件标识。
- 把加载后的class类信息存放于方法区,除了类信息之外,方法区还会存放运行时常量池信息,可能还包括字符串字面量和数字常量(这部分常量信息是Class文件中常量池部分的内存映射);
ClassLoader(类加载器)
只负责class文件的加载,至于它是否可以运行,则由Execution Engine(执行引擎)
决定;- 如果调用构造器实例化对象,则该对象存放在堆区;
- 所以说类加载子系统的目的就是将对应的字节码文件加载到方法区中
2. 类装载子系统加载流程
- 字节码文件以二进制流的形式加载到内存中,根据字节流生成一个运行时的数据区,经过类加载子系统的
加载阶段
,形成.class文件(Class),放入方法区中 - 链接阶段:
- 验证:验证文本的格式,源数据和字节码
- 准备:给类分配一个内存,给类的初始化变量做一个例如0的初始化赋值
- 解析:类中的字段,方法,符号,会把一些符号引用解析称为直接引用
- 初始化阶段:执行类的构造方法
clinit
的过程,把static模块
和static的变量
进行初始化赋值的一个过程 - 整个类装载子系统加载的流程是在方法区中去做这个事情
- 如果调用构造器实例化对象,则该对象存放在堆区;
- 如果有方法的调用就会压栈,在对应的线程中生成栈帧,之后又PC寄存器完成指定的调用
- 最终程序想要运行还得需要执行引擎进行执行,执行引擎还分为:解释器,即时编译器,分析器,垃圾回收器
3. 类加载器ClassLoader角色
.class文件
存在于本地硬盘上,可以理解为设计师画在纸上的模板,而最终这个模板在执行的时候是要加载,通过类加载器把它加载到方法区中,加载到方法区中会生成一个Class文件,到JVM当中来根据这个文件实例化出n个一模一样的实例。.class文件
加载到JVM中生成Class文件,改文件被称为DNA元数据模板(Class文件)
,这个Class文件放在方法区,并不在堆中。- 可以通过
Class文件
加载实例,这些实例都可以放在堆空间中 - 也可以通过getClassLoader()方法知道是那个类加载器加载的我们字节码文件,从而得到对应的类加载器
- 同时在堆中的实例也可以通过getClass()方法知道是哪个模板生成的
- 在
.class文件
--> JVM --> 最终成为元数据模板(Class文件)
,此过程就要一个运输工具(类装载器Class Loader),扮演一个快递员的角色。 - 最终成为元数据模板
(Class文件)
放在了方法区 - 元数据模板
(Class文件)
生成的实例放入堆中
4. 类加载器子系统内部执行过程: 类使用的7个阶段
- 类从被加载到虚拟机内存中开始,到卸载出内存,它的整个生命周期包括:
- 加载(Loading):通过类的全路径名获取二进制流,把流加载到方法区,生成一个Class文件,作为对应的模板
- 验证(Verification):加载完之后验证文本格式,源数据,字节码,符号引用
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initiallization)
- 使用(Using)
- 卸载(Unloading)这7个阶段。
- 其中验证、准备、解析3个部分统称为连接(Linking),这七个阶段的发生顺序如下
- 其中,加载、验证、准备、初始化、卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段不一定:它在某些情况下可以初始化阶段之后在开始,这是为了支持Java语言的运行时绑定(也称为动态绑定)。
- 加载、验证、准备、解析、初始化这五个步骤组成了一个完整的类加载过程。
- 使用没什么好说的,卸载属于GC的工作 。
5. 类加载的执行过程: 加载
- 加载是类加载的第一个阶段。有两种时机会触发类加载:
预加载
:- 虚拟机启动时加载,加载的是
JAVA_HOME/lib/
下的rt.jar
下的.class
文件,这个jar包里面的内容是程序运行时非常常常用到的,像java.lang.*
、java.util.
、java.io.
等等 - 因此随着虚拟机一起加载。要证明这一点很简单,写一个空的
main
函数,设置虚拟机参数为"-XX:+TraceClassLoading"
来获取类加载信息
- 虚拟机启动时加载,加载的是
运行时加载
:- 虚拟机在用到一个
.class
文件的时候,会先去内存中查看一下这个.class
文件有没有被加载,如果没有就会按照类的全限定名来加载这个类。 - 加载阶段做了有三件事情:
- 将
.class
文件以二进制字节流(通过全限定类名获取二进制流是外部获取的,实现这部分是类加载器做的)
的方式进行加载 - 将类信息、静态变量、字节码、常量这些.class文件中的内容放入方法区中
- 在内存中生成一个代表这个.class文件的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。一般这个Class是在堆里的,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的
- 将
- 虚拟机在用到一个
6. 类加载的执行过程: 连接
连接包含三个步骤
: 分别是验证Verification
,准备Preparation
,解析Resolution
三个过程
7. 类加载的执行过程: 连接
中的验证
过程
验证阶段
:这一阶段的目的是为了确保.class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全- Java语言本身是相对安全的语言(相对C/C++来说),但是前面说过,
.class
文件未必要从Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生.class文件
。在字节码语言层面上,Java代码至少从语义上是可以表达出来的。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃
,所以验证是虚拟机对自身保护的一项重要工作。 验证阶段将做一下几个工作
,如果没有问题说明是安全的- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
8. 类加载的执行过程: 连接
中的准备
过程
- 准备阶段是正式为
类变量(被static修饰的变量)
分配内存并设置其初始值的阶段,这些变量所使用的内存都将在方法区中分配 - 这时候进行内存分配的仅仅是
类变量(被static修饰的变量)
,会放在方法区中。而不是实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆
中 - 这个阶段赋初始值的变量指的是那些不被
final修饰的static变量
比如
“public static int value = 123”,value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如
"public static final int value = 123;"就不一样了,在准备段,虚拟机就会给value赋值为123。注意
:- 由于局部变量不像类变量那样存在准备阶段。类变量有两次赋初始值的过程,一次在
准备阶段
,赋予初始值(也可以是指定值);另外一次在初始化阶段
,赋予程序员定义的值。 - 因此,即使程序员没有为类变量赋值也没有关系,它仍然有一个默认的初始值。但局部变量就不一样了,如果没有给它赋初始值,是不能使用的。
- 由于局部变量不像类变量那样存在准备阶段。类变量有两次赋初始值的过程,一次在
9. 类加载的执行过程: 连接
中的解析
过程(符号引用)
解析阶段
是虚拟机将常量池内的符号引用替换为直接引用的过程。符号引用和直接引用有什么区别
:符号引用是一种定义
,可以是任何字面上的含义,而直接引用就是直接指向目标的指针、相对偏移量。这个其实是属于编译原理方面的概念,符号引用包括了下面三类常量:- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
举个例子
:
(1) 一套简单的代码:如下
package com.lagou.test;
public class TestMain {
private static int i;
private double d;
public static void print() {
}
private boolean trueOrFalse(){
return false;
}
}
(2) 用javap把这段代码的.class反编译一下:
- 看到
Constant Pool
也就是常量池中有22项
内容,其中带"Utf8"
的就是符号引用
。 - 比如
#2
,它的值是"com/xrq/test6/TestMain"
,表示的是这个类的全限定名; - 又比如
#5为i,#6为I
,它们是一对的,表示变量时Integer(int)类型的,名字叫做i; #8为D、#7为d
也是一样,表示一个Double(double)类型的变量,名字为d;#18、#19
表示的都是方法的名字。- 总而言之,符号引用和我们上面讲的是一样的,是对于
类、变量、方法
的描述。符号引用
和虚拟机的内存布局是没有关系的,引用的目标未必已经加载到内存中了。
10. 类加载的执行过程: 连接
中的解析
过程(直接引用)
直接引用
可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用
是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。- 如果有了
直接引用
,那引用的目标必定已经存在在内存中
了。
11. 类加载的执行过程: 连接
中的解析
过程(总结)
- 解析阶段负责把
整个类激活
,串成一个可以找到彼此的网,过程不可谓不重要。那这个阶段都做的工作大体可以分为:类或接口的解析
类方法解析
接口方法解析
字段解析
12. 类加载的执行过程: 初始化
- 类的
初始化阶段
是类加载过程的最后一个步骤, 之前介绍的几个类加载的动作里, 除了在加载阶段
用户应用程序可以通过自定义类加载器的方式局部参与外, 其余动作都完全由Java虚拟机来主导控制。 - 直到
初始化阶段
, Java虚拟机才真正开始执行类中编写的Java程序代码, 将主导权移交给应用程序。 - 初始化阶段就是执行类构造器
<clinit>()方法(类构造方法)
的过程,类构造器<clinit>()方法
的作用是收集一些静态变量
和执行一些静态代码块
。 <clinit>()
并不是程序员在Java代码中直接编写的方法, 它是Javac编译器的自动生成物,<clinit>()
方法是由编译器自动收集类中的所有类变量(静态变量)
的赋值动作和静态语句块(static{}块)
中的语句合并产生的, 编译器收集的顺序是由语句在源文件中出现的顺序决定的, 静态语句块中只能访问到定义在静态语句块之前的变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不能访问<clinit>()
方法与类的构造函数(即在虚拟机视角中的实例构造器<clinit>()
方法) 不同, 它不需要显式地调用父类构造器, Java虚拟机会保证在子类的<clinit>()
方法执行前, 父类的<clinit>()
方法已经执行完毕。 因此在Java虚拟机中第一个被执行的<clinit>()
方法的类型肯定是java.lang.Object
。- 由于父类的
<clinit>()
方法先执行, 也就意味着父类中定义的静态语句块要优先于子类的变量赋值操作
package com.lagou.unit2;
class TestClinit02 {
static class Parent {
public static int A = 1;
static {
A = 2;
}
}
static class Sub extends Parent {
public static int B = A; // 2
}
public static void main(String[] args) {
System.out.println(Sub.B); // 2
}
}
<clinit>()
方法对于类或接口来说并不是必需的, 如果一个类中没有静态语句块, 也没有对变量的 赋值操作, 那么编译器可以不为这个类生成<clinit>()
方法。 接口中不能使用静态语句块, 但仍然有变量初始化的赋值操作, 因此接口与类一样都会生成<clinit>()
方法。- 但接口与类不同的是, 执行接口的
<clinit>()
方法不需要先执行父接口的<clinit>()
方法, 因为只有当父接口中定义的变量被使用时, 父接口才会被初始化。 此外, 接口的实现类在初始化时也 一样不会执行接口的()方法。 - Java虚拟机必须保证一个类的
<clinit>()
方法在多线程环境中被正确地加锁同步, 如果多个线程同 时去初始化一个类, 那么只会有其中一个线程去执行这个类的<clinit>()
方法, 其他线程都需要阻塞等待, 直到活动线程执行完毕<clinit>()
方法。 如果在一个类的<clinit>()
方法中有耗时很长的操作, 那就可能造成多个进程阻塞, 在实际应用中这种阻塞往往是很隐蔽的
13. <cinit>
与<init>
的区别
-
主要是为了让你弄明白类的初始化
<cinit>
和对象的初始化<init>
之间的差别。 -
到底是谁在前,谁在后
-
答案:
-
一个类加载的过程是先加载,连接,之后初始化,之后执行clinit方法
-
clinit方法里面执行静态代码块和静态变量的赋值
-
类加载的时候早于对象创建的过程
-
只有对象创建的过程才会对成员变量进行赋值,然后调用构造函数,所以说clinit方法早于init方法的执行
-
其中
static字段
和static代码块
,是属于类的,在类的加载的初始化阶段就已经被执行。类信息会被存放在方法区,在同一个类加载器下,这些信息有一份就够了,所以上面的static代码块
只会执行一次,它对应的是方法。
-
所以,上面代码的 static 代码块只会执行一次,对象的构造方法执行两次。再加上继承关系的先后原则,不难分析出正确结果。
-
结论:
- 方法的执行时期:
<cinit>()
优先<init>()
执行 <cinit>()
方法的执行时期: 类初始化阶段(该方法只能被jvm调用, 专门承担类变量的初始化工作) ,只执行一次<init>()
方法的执行时期: 对象的初始化阶段
- 方法的执行时期: