java虚拟机类加载机制

类加载机制

一 类加载时机

类从被加载到虚拟机内存中开始,到卸载出内存为止,生命周期包括:
java虚拟机类加载机制
其中,加载、验证、准备、初始化和卸载这五个步骤的顺序是确定的,解析阶段则在某些情况下可以在初始化后再开始,这是为了支持Java的动态绑定。

对于初始化阶段,虚拟机规范则严格规定有且只有五种情况必须立即对类进行初始化:

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。最常见场景:使用new实例化对象时、读取或设置一个类的静态字段(被final修饰,已在编译期放入常量池的静态字段除外)时,以及调用一个类的静态方法时。
  2. 使用java.lang.reflect包的方法对类进行反射调用时,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类时,如果其父类没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个主类。
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。

这五种场景的行为成为对一个类进行主动引用,除此之外,所有引用类的方式都不会触发初始化,成为被动引用。例:

  1. 通过子类引用父类的静态字段,会触发父类的初始化,不会导致子类的初始化。
  2. 通过数组定义来引用类,不会触发此类的初始化。
  3. 常量在编译阶段回存入调用类的常量池中,本质上没用直接引用定义常量的类,所以不会触发定义常量的类的初始化。

接口也有初始化过程,区别在于上述第三点,当一个类初始化时,要求父类全部都初始化过,但是接口在初始化时,并不要求其父接口全部完成初始化,只有在真正使用父接口时(如引用父接口中定义常量)才会初始化。

二 类加载过程

1. 加载

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

虚拟机规范多上面这3点并不具体,因此是非常灵活的。比如:“通过全类名获取定义此类的二进制字节流” 并没有指明具体从哪里获取、怎样获取。比如:比较常见的就是从 ZIP 包中读取(日后出现的JAR、EAR、WAR格式的基础)、其他文件生成(典型应用就是JSP)等等。

一个非数组类的加载阶段(加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,这一步我们可以去完成还可以自定义类加载器去控制字节流的获取方式(重写一个类加载器的 loadClass() 方法)。数组类型不通过类加载器创建,它由 Java 虚拟机直接创建。

2. 验证

验证是为了确保Class文件的字节流包括的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  1. 文件格式验证
    保证输入的字节流能正确地解析并存储于方法区之内,格式上符合一个Java类型信息的要求。这个阶段是基于二进制字节流进行的,只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面的三个验证阶段全部给予方法区存储结构,不会直接操作字节流。
  2. 元数据验证
    对字节码描述的信息进行予以分析,对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
  3. 字节码验证
    如果一个类方法体的字节码没有通过字节码验证,肯定有问题;但如果通过了,不能说明一定安全。
  4. 符号引用验证
    发生在虚拟机将符号引用转为直接引用的时候(解析阶段)。对类自身外(常量池中的各种符号引用)的信息进行匹配性校验,确保解析动作能正常执行。

java虚拟机类加载机制

3. 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在 Java 堆中。
  • 这里所设置的初始值"通常情况"下是数据类型默认的零值(如0、0L、null、false等),比如我们定义了public static int value=111 ,那么 value 变量在准备阶段的初始值就是 0 而不是111(初始化阶段才会赋值)。特殊情况:比如给 value 变量加上了 final 关键字public static final int value=111 ,那么准备阶段 value 的值就被赋值为 111。
    基本数据类型的零值:
    java虚拟机类加载机制

4. 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
在程序实际运行时,只有符号引用是不够的,举个例子:在程序执行方法时,系统需要明确知道这个方法所在的位置。Java 虚拟机为每个类都准备了一张方法表来存放类中所有的方法。当需要调用一个类的方法的时候,只要知道这个方法在方发表中的偏移量就可以直接调用该方法了。通过解析操作符号引用就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用。
综上,解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,也就是得到类或者字段、方法在内存中的指针或者偏移量。

5. 初始化

初始化是类加载的最后一步,也是真正执行类中定义的 Java 程序代码(字节码)的阶段。初始化阶段是根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以说,初始化阶段是执行类构造器 ()方法的过程。

  • ()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的。静态语句块中只能访问到定义在静态语句块之前的变量,之后的可以赋值不能访问。
  • 虚拟机保证在子类的()方法执行前,父类的()方法已经执行完成。
  • 由于父类的()先执行,所以父类中定义的静态语句块要先于子类的变量赋值操作。
  • 执行接口的()不需要先执行父接口的()方法。接口的实现类初始化时也不会执行接口的()。
  • 对于() 方法的调用,虚拟机会自己确保其在多线程环境中的安全性。因为 () 方法是带锁线程安全,所以在多线程环境下进行类初始化的话可能会引起死锁,并且这种死锁很难被发现。