概述
从类被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期分为7个阶段,加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading)。其中验证、准备、解析三个部分统称为链接。具体的顺序如图:

值得注意的是,加载、验证、准备、初始化、卸载的顺序是固定的,但是为了支持java语言的运行时绑定(多态),解析过程是有可能发生在初始化之后的。
同时,对于加载来说,它的输入仅仅要求是符合class格式规范的二进制流即可,至于二进制流的来源,并没有做约束。
加载
虚拟机在加载阶段,主要工作如下:
- 通过
类的全限定名获取该类的二进制字节流; - 将字节流所代表的静态存储结构 转化为 方法区的运行时数据结构;
- 生成代表该类的Class对象并存放于堆中,作为方法区该类的各种元数据的访问入口。
关于Class对象是存放在哪个内存区域,可以参考:java中的静态变量和Class对象究竟存放在哪个区域?
链接(Linking)
验证
验证是连接阶段(Linking)的第一步,目的是为了确保Class文件的字节流符合虚拟机规范,不会危害虚拟机自身安全。比如:访问数组越界问题,将对象转型为未实现的类型,跳转到不存在的代码区等情绪编译器都会拒绝编译,也就是无法生成Class文件,既然如此,为什么还要验证呢?原因是Class文件不一定都是由java源码编译而成,可以是任何途径,所以验证还是很有必要的,尽可能保证系统能承受住恶意代码攻击。
验证主要工作分4阶段:
文件格式验证:验证是否符合Class文件格式规范;
元数据验证:验证是否符合Java语言规范;
字节码验证:验证数据流和控制流分析;
符号引用验证:验证符号引用转化为直接引用。
准备
准备阶段的主要工作是为static变量进行内存分配或赋值,这些变量所用的内存都会在方法区分配。
- 类变量,赋初始零值。例如对于
public static int a = 1;会在该阶段赋a值为0。 - 常量:赋真实值。例如对于
public static final int a = 1;会在该阶段赋a值为1。 - 实例变量:不分配内存,赋值为null。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
细节可以参考:JVM符号引用转换直接引用的过程?
初始化
初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。
初始化的时机
虚拟机规范没有对类的加载时机做强制约束,但是对于初始化阶段,虚拟机规范严格规定了有且仅有5种情况需要对类进行初始化,同时显然加载、验证、准备过程也要在这之前完成。
主动引用
必然会触发类初始化的类引用称为主动引用,具体场景如下:
- 遇到new、getstatic、putstatic或invokeStatic这4条字节码指令时。常见场景:使用new关键字实例化对象时,触发new;读取类变量时,触发getstatic;(final常量除外)设置类变量时,触发putstatic;调用类的静态方法时,触发invokeStatic;
- 初始化一个类时,当其父类没有初始化,则需要先触发其父类的初始化;
- 使用java.lang.reflect包中的方法对类进行反射调用时;
- 虚拟机启动时,需指定一个要执行的主类(含有main()的类),虚拟机会先初始化该类;
- 当java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且该句柄所对应的类没有进行过初始化;
被动引用
除去上面五种对类的引用外,其他对类的引用不会触发类的初始化,被称为被动引用,场景如下:
- 通过子类引用父类的静态(static)字段,不会导致子类初始化。
- 通过数组定义来引用类,不会触发此类的初始化。因为声明某个类型的数组,会直接引用到JVM生成的数组类,而不是该类型对应的类。例如
Test[] t = new Test[1];会触发类[com.xybean.Test的初始化,而类com.xybean.Test并不会被初始化。 - 常量(static final)在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
初始化过程
初始化阶段是执行类构造器\
- \
()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。 - \
()方法与实例构造器\ ()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的\ ()方法执行之前,父类的\ ()方法已经执行完毕。因此,在虚拟机中第一个被执行的\ ()方法的类肯定是java.lang.Object。 - \
()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成\ ()方法。 - 接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成\
()方法。但是接口与类不同的是:执行接口的\ ()方法不需要先执行父接口的\ ()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的\ ()方法。 - 虚拟机会保证一个类的\
()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的\ ()方法,其他线程都需要阻塞等待,直到活动线程执行\ ()方法完毕。如果在一个类的\ ()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。
类加载器与双亲委派
类加载器的作用就是通过一个类的全限定名来获取描述此类的二进制字节流。
类加载器类型
- 启动类加载器(Bootstrap ClassLoader):负责加载 JAVA_HOME\lib 目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可(按文件名识别,如rt.jar)的类。
- 扩展类加载器(Extension ClassLoader):负责加载 JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs系统变量指定路径中的类库。
- 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。如果应用程序中没有自定义过自己的类加载器,那么所有的用户类都由这个类加载器加载。
双亲委派
双亲委派指的是如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。
具体的过程如图:

具体的代码逻辑可以查看ClassLoader中loadClass()的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
代码逻辑清晰,可以自解释,就不赘述了。
实现自己的ClassLoader
从上面的ClassLoader基类的代码可以看出我们想要实现自己的ClassLoader,只要实现findClass()就可以了。
1 |
|
参考资料
Java Virtual Machine Specification > Chapter 5. Loading, Linking, and Initializing