类加载
一个类从被加载到虚拟机内存开始,到卸载出内存,它会经历加载、连接(验证,准备,解析)、初始化、使用和卸载这5个阶段。前3个阶段又被统称为类加载。
1、加载
加载是整个类加载过程中的第一个阶段。在加载阶段,JVM
需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流;
- 将这个
字节流
所代表的静态存储结构转化为方法区
的运行时数据结构; - 在内存中生成一个代表这个类的
Class
对象,作为方法区
中这个类的各种数据的访问入口;
简单点讲就是把由class文件
生成的二进制字节流存在方法区
,并在堆
中生成一个Class
对象指向这个二进制字节流。
2、验证
验证是连接的第一步,它的任务是确保Class
文件的字节流中包含的信息符合*《Java虚拟机规范》*的要求,保证代码在运行时不会对虚拟机产生危害。
Class
文件实际上就是一个二进制文件,开发者自己就可以在二进制编辑器中敲出来,不一定非要通过Java
代码编译得到,所以一些Java
代码实现不了的事情就可以通过自定义的Class
文件实现。自定义的Class
可能会有一些威胁虚拟机的操作,所以虚拟机需要先验证所有的Class
文件的字节流。
在验证阶段,JVM
需要完成下面这些检查:
- 验证文件格式,比如看
Class
文件是否以0xCAFEBABE
开头,常量池中常量是否有不支持的常量等等。这个阶段的验证是基于二进制字节流的,文件格式没问题之后,字节流才被允许被放入方法区。后面的3个验证都是基于方法区中的存储结构进行的。 - 验证元数据,元数据是用来描述类之间关系的数据。这一阶段会验证如是否有父类(
java
中除了Object
类,其他的类都应该有父类),是否实现了其父类中要求实现的方法等等。 - 验证字节码,即验证类中的方法体,确保类中的方法不会做出危害虚拟机的行为。
- 验证符号引用,发生在虚拟机将符号引用转化为直接引用的时候(即连接中的解析阶段)。主要是看常量池中通过字符串描述的类,方法,字段是否存在。
只要通过了验证阶段,就表示加载进来的Class
是安全的。在生产环境中,如果能确保全部代码没有问题,可以通过来-Xverify:none
关闭验证措施,缩短类加载时间。
3、准备
准备阶段会为静态变量(类变量)分配内存并附初始值,比如在类中定义了一个用static
修饰的int
变量i
,此阶段就会将i
的值设为0
。
public static int i = 2;
需要注意的是经过准备阶段,这个i
的值是0
而非2
。真正将i
的值变为2
是在初始化阶段完成的。
还有一种情况是当一个变量用final
修饰时,会将其设定为所指定的值。下面的i
经过准备阶段,值为2
。
public static final int i = 2;
4、解析
解析阶段会将常量池内的符号引用替换为直接引用。如果不了解符号引用,可以看下面这个例子。
首先定义一个最简单类HelloWorld
,经过javac HelloWorld.java
编译后得到了HelloWorld.class
public class HelloWorld {
}
用Sublime
打开HelloWorld.class
就可以得到其16
进制格式文件。下面的分析建议结合后面用javap
反解析得到的内容一起看。0x000d
表示的是常量池的长度,0x0a
表示这是一个方法引用CONSTANT_Methodref_info
,后面0x0003
表示这个方法所在的类是#3
常量,0x000a
表示这个方法的描述符是#10
常量。#3
常量是一个类引用常量CONSTANT_Class_info
,它引用了#12
号常量。#12
常量是一个字面量"java/lang/Object"
。
同上,#10
引用了#4
,#5
,最后得到了字面量"<init>:()v"
。
由此就可以知道常量池中第一项就表示了一个Object
类中一个无参返回值为空的方法。
这是通过javap -v HelloWorld.class
分析得到的Class
内容,只截取出了常量池部分。
上面分析中CONSTANT_Methodref_info
和CONSTANT_Class_info
都是符号引用。如果想要详细了解可以学习Class
文件格式。符号引用描述了类,方法等在什么包中,长什么样,但是并没有指定这个类在实际的内存中的具体位置。
直接引用是一个直接指向目标的指针,它指向的是目标在内存的实际位置。
在解析阶段,就是将常量池中的符号引用解析成了直接引用。经过解析,JVM
才能在内存中实际找到目标的位置。
5、初始化
初始化是类加载的最后一个阶段,这一阶段会执行类构造器<clinit>()
方法。<clinit>()
方法是编译器自动收集类中的赋值动作和静态代码块中的语句合并产生的。简单讲就是给静态变量赋初始值并执行静态代码块。
注意这一阶段不会执行构造方法,要分清类加载和实例化。具体验证可以看下面这个例子。我定义了一个类HelloWorld
,里面有一个静态语句块和一个构造方法。
之后在Test类中加载HelloWorld但并没有实例化
此时控制台只输出了"我是静态方法"
,即说明初始化阶段只并不会执行构造方法。
6、类加载小结
下面对5个阶段各自的工作做一个简单的总结:
加载(Loading)
:将Class
文件生成的二进制流存入方法区,在堆中生成一个Class
指向二进制流
验证(Verification)
:验证字节码是否符合规范
准备(Preparation)
:给静态变量赋默认值
解析(Resolution)
:把符号引用转成直接引用
初始化(Initialization)
:将静态变量赋初始值
7、类加载器
类加载器实现了类的加载动作,有3种系统提供的类加载器
Bootstarp ClassLoader
:启动类加载器,负责加载lib
目录种的类库,如rt.jar
等Extension ClassLoader
:拓展类加载器,负载加载lib\ext
目录种的类库。这个类库中是一些用于扩展JavaSE
内容的类Application ClassLoader
:应用程序类加载器,负责加载用户类。一般我们自己定义的类就是有这个加载器加载的。
8、双亲委派机制
用一个例子来说明双亲委派机制。
现在我们要加载一个自定义的类Test
,首先从自定义类加载器开始,如果我们没定义加载器,就是从应用程序类加载器开始。自定义类加载器会先看看自己是否加载过这个类,加载过就直接返回,如果发现自己没加载过,会委派其父加载器应用程序类加载器去加载。同样地,应用程序类加载器也是先看看自己有无加载过,有直接返回,没有就委派给上一级。
一直委派到启动类加载器,它会到自己的加载范围搜寻这个类,发现没有时,就会开始向下委派。每一级都会到自己的加载范围中寻找这个类,如果在自己的加载范围类,就会进行加载并返回,如果没有,就会委派给下一级去加载。
9、双亲委派机制源码
ClassLoader
类中定义loadClass
方法用于加载类,下面的代码为了弄清楚逻辑省去了一些。从下面的代码中就可以清楚地看出类加载机制的实现。
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
// 1、先检查自己是否加载过这个类
Class<?> c = findLoadedClass(name);
// 2、如果没有,向上委派
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 3、一直委派到启动类加载器,它会调用findClass方法试图加载这个类
if (c == null) {
c = findClass(name);
}
}
// 4、加载成功就返回
return c;
}
}