引用官方:
虚拟机把描述类的数据从Class 文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型叫做虚拟机的类加载机制。
在详细介绍加载的每隔步骤之前,先来了解下总体步骤:
- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载
从上面我们可以看出,一个类从加载到卸载总共会经历七个步骤。其中,验证、准备、解析三个步骤又被统称为连接。
下面开始详细分析介绍这七个步骤中的每一步:
什么时候会触发类的初始化动作?
1、new、putstatic、getstatic、invokestatic 4条字节码指令,如果一个类还没有初始化则会触发初始化操作(加载动作会发生)。在java代码中对应的便是:new Object(),使用某个类的静态属性(又叫类属性),使用某个类的静态方法。
2、使用java.lang.reflect方法的时候
3、初始化一个类的时候,发现其父类还没有进行初始化
4、当虚拟机启动的时候,用户指定的执行的主类(main()方法)
在虚拟机规范中,把类的引用分为了主动引用和被动引用,以上四种方式被称作主动引用,同时有且仅有以上四种方式被称为主动引用。下面为大家介绍几种被动引用的列子:
- 子类引用父类的静态字段不会触发子类的初始化
public class father {
static {
Syso("class father 被初始化辣------!");
}
//这里需要区分 : public static final int value = 100的区别(后面会讲到)
public static int value = 100;
}
public class son extends father {
static {
Syso("class son 被初始化辣-------!");
}
}
public class main {
public static void main(String [] args ) {
Syso(son.value);
}
}
- 通过数组定义来引用类不会触发该类的初始化
//这里继续使用上面定义的son 类
public class main{
public static void main(String[] args) {
son[] sons = new son[10];
}
}
运行上面这段代码,我们会发现并没有 "class son 被初始化辣!"这段话输出,说明son这个类没有被初始化,这是为什么呢?
原因是:
虚拟机在这里触发了另一个类的初始化,他就是:Lorg.fenixsoft.classloading.son .
这个类是由虚拟机直接创建,并继承与Object类,会由指令 newarray触发
- 引用类常量不会触发常量类的初始化
class Constclass {
static{
Syso("Constclass 初始化辣!")
}
public static final String value = "hello 世界你好!!";
}
class Main{
public void main(String[] args) {
Syso(Constclass.value);
}
}
运行上面的饿代码,我们不会看到 "Constclass 初始化辣!"这行文字的输出。是因为在编译阶段将常量类Constclass的常量value存储到了Main类的常量池中。
上面唠叨了一堆,为大家介绍了类加载会经理哪些过程,以及在什么时候会触发类的初始化。以及在实际使用中常常会被我们忽略的被动初始化情况。接下来为大家详细介类加载过程中每一步的详情。
加载
在加载阶段,jvm主要会做三件事:
- 根据类的全限定名(在反射的时候会用到Class.forName(“类的全限定名”))来获取定义此类的二进制字节流文件
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在堆区中生成一个代表这个类结构的 java.lang.Class 对象,作为方法区这些数据的访问入口。
在整个类加载过程中,加载阶段是可控性最强的,用户可以自定义类加载器来完成一些特别的事情。在代理模式中也主要是在这个阶段做文章。(稍后会讲到类加载器知识点,会在另一篇笔记中探讨关于代理的知识点)
验证
这个阶段的主要目的是为了保证Class文件的字节流中包含的信息符合当前虚拟机的要求。不同虚拟机的验证规则和验证步骤会由些许不同,但大致主要分为以下几个步骤:
- 文件格式验证(这个阶段是为了保证字节流能够被正确的解析并存储于方法区中,经过这个阶段的验证,字节流才会进如内存的方法区中进行存储)
- 元数据验证(主要工作室:字节码描述的信息进行语义分析,目的:对类的元数据信息进行语义校验)
- 字节码验证(主要工作是对数据流和控制流进行分析,这部分的验证主要是针对方法体进行)
- 符号引用验证(发生在虚拟机将符号引用转换为直接引用的阶段,也就是类加载的解析阶段)
准备
在这个阶段将会为类变量分配类存和设置初始化值,这些内存会被分配在方法区中(这里需要搞清楚类变量和实化变量,同时,这里所说的初始化值是指数据类型的零值)。
问题1:
public static int value = 123;
请问:在准备阶段,value的值等于多少?
答案是:0
为什么呢?
是因为在这个时候还未执行任何java方法,把value的值设置成123的putstatic指令是在程序编译后,存放于类构造器中,value =123在类的初始化阶段才会执行。
问题2:
public static final value =123;
请问:在准备阶段,value的值等于多少?
答案是:123
解析
解析属于类加载的第四个阶段,在这个阶段,主要的目的是将常量池中的符号引用转换成直接引用。
在这个阶段,我们需要搞清楚几个问题。
问题1:符号引用和直接引用的区别?
符号引用:用一组符号来表示引用的目标。符号引用与内存布局无关,引用的目标并不一定加载到内存中去了。
直接引用:直接引用可以是直接指向目标的指针或者位置偏移量,或者是间接指向目标的句柄。直接引用与虚拟机的内存布局有关,如果有了直接引用,那引用的目标必定已经存在于内存布局当中。
问题2:解析的动作主要针对谁?
一般解析主要针对类、接口、字段、类方法。
初始化
初始化是类加载的最后一个阶段,也是在初始化阶段才是真正开始执行java代码(字节码文件)
在准备阶段,执行过一次类变量赋值操作(记住这时的赋值特点)。在初始化阶段,会真正按照程序员的计划去执行赋值操作,初始化阶段又叫做执行类构造器()方法的过程。
在这里有几个点需要注意:
- 这个方法由编译器自动收集类变量的赋值操作和类中的静态语句块中的语句合并而成的。
- 编译器可以保证一个类的()方法在多线程环境下面可以被正确的加锁执行。所以我们需要注意,不能再类初始化方法()方法中执行耗时操作。如下代码便是一个错误的写法:
class MyClass {
static {
while(true) {
Syso(“模拟耗时操作---------”);
}
}
}