JVM: 类加载(1)-类加载机制

类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。

Java可以动态扩展的特性就是依赖运行期动态加载和动态连接这个特点实现的。

类加载的时机

类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的(这些阶段通常互相交叉地混合式进行,在一个阶段执行的过程中激活另一个阶段),而解析阶段在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定)。

类初始化时机

虚拟机规范规定有且只有5种情况(也被称为主动引用),如果类没有进行初始化,则需要先触发其初始化。

  1. 遇到newgetstaticputstaticinvokestatic这4条字节码指令时。
    常见的场景:使用new关键字实例化对象、读取或调用一个类的静态字段的时候(被final修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法的时候。
  2. 反射调用。
  3. 初始化一个类时,其父类还没有进行初始化,先触发父类的初始化。(接口并不要求其父接口都完成初始化)
  4. 虚拟机启动时,要执行的主类(包括main()方法),虚拟机会先初始化这个主类。
  5. 使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStaticREF_putStaticREF_invokeStatic的方法句柄,方法句柄对应的类没有初始化,则需要先触发其初始化。

Reflection和MethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。
相关知识点可以参见:
JVM: 字节码执行引擎(2)-方法调用
http://www.infoq.com/cn/articles/jdk-dynamically-typed-language

几个不会触发类初始化(也被称为被动引用)的例子:

  1. 通过子类引用父类的静态字段,不会导致子类初始化。System.out.println(SubClass.value);value是父类静态字段)
  2. 通过数组定义来引用类,不会触发类的初始化。MyClass[] arr = new MyClass[10];
  3. 使用常量(编译阶段会存入调用类的常量池中)。

类加载的过程

类加载的过程就是:加载、验证、准备、解析和初始化这5个阶段。

加载

加载阶段,虚拟机完成以下3件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流(一般是字节码文件)。
  2. 将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

加载阶段中获取获取类的二进制字节流,非数组类由类加载器去完成;数组类情况不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。

加载阶段完成后,二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自定义。然后在内存中实例化一个java.lang.Class对象(并没有明确规定是在Java堆中,HotSpot虚拟机把Class对象存在方法区中),作为程序访问方法区中这些类型数据的外部接口。

加载阶段可以由用户自定义的类加载器去完成,其他阶段都是由虚拟机主导和控制的。

验证

验证是连接的第一步,目的是为了确保二进制字节流中所包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证阶段大致会完成4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

文件格式验证

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

通过了这个验证字节流才会进入内存的方法区中进行存储,后面3个验证部分都市基于方法区的存储结构进行的,不会再直接操作字节流。

元数据验证

阶段二:元数据验证就是对字节码所描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求。

字节码验证

阶段三:字节码验证主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

阶段二对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。

符号引用验证

最后一个阶段的校验发生在虚拟机将符号引用转换为直接引用的时候,这个转换动作发生在解析阶段(连接的第三阶段)。目的是确保解析能正常执行。

准备

准备阶段正式为类变量(static修饰的变量)分配内存并设置类变量初始值(一般是数据类型的零值),这些变量所用内存都在方法区中进行分配。
如果类变量是常量,那么在准备阶段变量值就会被初始化为其指定的值。

实例变量会随着对象一起分配在Java堆中。

解析

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

  • 符号引用(Symbolic References):定义在Java虚拟机规范的Class文件中,符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量。
  • 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,有了直接引用,那引用的目标必定已经在内存中存在。

比如类或接口解析、字段解析、类方法解析、接口方法解析以及动态调用相关的解析。

初始化

类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的Java代码(字节码)。

在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

初始化阶段是执行类构造器<clinit>()方法的过程。同一个类加载器下,一个类型只会初始化一次。

<clinit>()方法

  • <clinit>()方法是由编译器合并所有类变量的赋值动作和静态语句块中的语句产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的。

    静态语句块只能访问定义在其之前的变量,定义在其之后的变量只能被赋值但不能被访问。

1
2
3
4
5
6
static{
i = 10; // ok
System.out.println(i); // compile error 不能访问,非法向前引用
}
static int i = 20; // 最后i的值还是20
  • <clinit>()方法不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。(虚拟机第一个被执行的<clinit>()方法的类肯定是java.lang.Object

  • <clinit>()方法对于类或接口并不是必需的,如果没有静态变量的赋值操作或静态语句块,那么编译器不会为这个类或接口生成<clinit>()方法。

  • 接口可以有静态变量赋值操作,但是不能使用静态语句块。执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

  • 虚拟机会保证<clinit>()方法在多线程环境下被正确地加锁、同步。