JVM的类加载机制

  虚拟机把描述类的数据从Class文件(这里所说的Class文件指一串二进制的字节流,无论以何种形式出现都可以)加载到内存,并对数据进行检验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。   

类加载的时机:

  从类被加载到虚拟机内存中开始,到卸载出内存为止,类的生命周期包括加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段,这里的“加载”是“类加载”过程的一个阶段,注意区分。其中验证、准备和解析三部分称为连接。这7个阶段顺序如下图所示:
  image_1b0juhg751g961i6p7mf3hkmmi9.png-59.9kB

  其中加载、验证、准备、初始化和卸载这5个阶段的顺序是固定的(即:加载阶段必须在验证阶段开始之前开始,验证阶段必须在准备阶段开始之前开始等。这些阶段都是互相交叉地混合式进行的,通常会在一个阶段的执行过程中调用或激活另一个阶段),解析阶段则不一定,在某些情况下,解析阶段有可能在初始化阶段结束后开始,以支持Java的动态绑定。
  什么时候开始第一个阶段——“加载”,JVM规范没有规定,可由不同的JVM实现自己把握,但是规定了“初始化”阶段的开始时机,下面5种情况下必须立刻进行类的“初始化”:

  1. 遇到new、getstatic、putstatic或invokestatic四条字节码指令时。如果类没有进行过初始化,则需先触发其初始化。这四条指令对应于Java代码就是:使用new实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的字段除外)的时候,及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect.*的方法对类进行反射调用的时候,如果类还没有进行过初始化,马上对其进行。
  3. 初始化一个类的时候,如果他的父亲还没有被初始化,则先去初始化其父亲。
  4. 当jvm启动时,用户需要指定一个要执行的主类(包含static void main(String[] args)的那个类),则jvm会先去初始化这个类。
  5. 当使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

  对于上述5种情况,JVM规范限定为“有且只有”,这5种场景中的行为成为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。下面举3个例子来说明何为被动引用:(包名统一为classLoading)

例1:

public class SuperClass {
       public static int value = 123;
       static {
           System.out.println("super class init.");
       }
   }

   public class SubClass extends SuperClass {
       static {
           System.out.println("sub class init.");
       }
   }

   public static void main(String[] args) {    
       System.out.println(SubClass.value);
   }

执行后输出的结果如下:

super class init.
123

  程序并不会输出“sub class init.”。通过子类引用父类中的静态字段,不会初始化子类,只会初始化父类。对于静态字段,只有直接定义这个字段的类才会被初始化,因此,通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。
  
例2:

public static void main(String[] args) {
        SuperClass[] arr = new SuperClass[10];
}

public class SuperClass {
    public static int value = 123;
    static {
        System.out.println("super class init.");
    }
}

  运行程序发现没有输出“super class init.”,说明没有触发SuperClass类的初始化阶段,但这段代码里触发了另一个名为“[LclassLoading.SuperClass”的类的初始化,它是一个由虚拟机自动生成的、直接继承于java.lang.Object的子类,创建动作由字节码指令newarray触发。
  
例3:

public class NotInitialization{ 
public static void main(String[] args) {
        System.out.println(ConstClass.Test);
        }
}

public class ConstClass {
    public static final String Test = "Hello world!";

    static {
        System.out.println("const class init.");
    }
}

程序运行结果:
Hello world!

  运行程序只会输出“Hello world!”,而不会输出“const class init.”,证明没有初始化ConstClass类。虽然程序中引用了ConstClass类的常量Test,但是在编译阶段将此常量的值“Hello world!”存储到了NotInitialization类的常量池中,以后NotInitialization对常量Test的引用实际上转化为了NotInitialization类对自身常量池的引用。也就是说,实际上NotInitialization的Class文件之中并没有ConstClass类的符号引用入口,这两个类在编译成Class文件后就不存在任何联系了。

类加载的过程

  JVM中类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。
  
一、加载:

   “加载”是“类加载”中的一个阶段,在加载阶段,虚拟机需要完成以下三件事。

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

二、验证:

  验证是连接阶段的第一步,这一步主要的目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身安全。验证阶段大致分为下面四个阶段:
1、文件格式验证:验证字节流是否符合Class文件格式的规范,并且是否能被当前版本的虚拟机处理(如:是否以魔数0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内等)。
2、元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范要求。主要目的是对类的元数据信息进行语义检验,保证不存在不符合Java语言规范的元数据信息。
3、字节码验证:整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完全检验后,这个阶段对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为。
4、符号引用验证:可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。

三、准备:

  准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。这个阶段中有两个容易混淆的点:
(1)这个时候进行内存分配的仅包含类变量(被Static修饰的变量),则不包括实例变量(实例变量将会在对象实例化时随着对象一起体在Java堆中)。
(2)这里所说的初始值“通常情况”下是数据类型的零值。假设一个类变量定义为:
public static int value = 123;
那么变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会被执行。

四、解析:

  解析阶段是虚拟机常量池内的符号引用替换为直接引用的过程。
  
符号引用(Symbolic References):符号引用是一组符号来描述所引用的目标对象,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因此符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用(Direct References):直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定已经在内存中存在。

  虚拟机规范并没有规定解析阶段发生的具体时间,只要求了在执行anewarry、checkcast等16个用于操作符号引用的字节码指令之前,先对它们使用的符号引用进行解析,所以虚拟机实现会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。
  解析的动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行。分别对应编译后常量池内的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四种常量类型。
  
五、初始化:

  类的初始化阶段是类加载过程的最后一步,在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划区初始化类变量和其他资源,或者可以说,初始化阶段是执行类构造器()方法的过程。