Fork me on GitHub

【JVM】Java类加载机制

Java类加载机制

1. 生命周期

类从加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期如下:

1532939355060

2. 加载过程

2.1 加载

在加载阶段,虚拟机需要完成以下3件事情

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。(这个Class对象并没有规定是在Java堆内存中,对于HotSpot虚拟机而言,它比较特殊,虽为对象,但存放在方法区中)

2.2 验证

这一阶段的主要目的是为了确保Class文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  • 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理

这阶段的验证是基于二进制字节流的,只有通过验证,字节流才会进入内存的方法区进行存储,后面的验证阶段是基于方法区的存储结构进行的

  • 元数据验证

对字节码描述的信息进行语义分析,保证描述的信息符合Java语言规范

  • 字节码验证

最复杂的阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的

  • 符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用时候,确保解析动作能正常进行。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验

2.3 准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。

通常情况下,初始值为零值,比如一个类变量定义为:

1
public static int v = 8080;

实际上变量v在准备阶段过后的初始值为0而不是8080,将v赋值为8080的putstatic指令是程序被编译后,存放于类构造器方法之中,将v赋值为8080的动作将在初始化阶段才会进行。

如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段v会初始化ConstantValue属性为所指定的值

1
public static final int v = 8080;

在编译阶段会为v生成ConstantValue属性,在准备阶段虚拟机会根据ConstantValue属性将v赋值为8080。

2.4 解析

解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程

  • 符号引用:与虚拟机实现的布局无关,引用的目标并不一定要已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
  • 直接引用:与虚拟机实现的布局相关,可以是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

2.5 初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由JVM主导。到了初始阶段,才开始真正执行类中定义的Java程序代码。

初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句合并而成的。虚拟机会保证方法执行之前,父类的方法已经执行完毕。如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成()方法。

注意以下几种情况不会执行类初始化:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  • 通过类名获取Class对象,不会触发类的初始化。
  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

3. 参考

《深入理解Java虚拟机》