类加载的七个阶段 - 图1开始顺序自上而下
_
要点:

  • 加载、连接和初始化阶段是在程序运行期间执行,这种策略给提前编译带来了额外的困难,也让类加载时增加了一些性能开销。但是却为java应用带来了极高的扩展性和灵活性——运行时动态加载和动态连接。
  • 加载、验证、准备、初始化和卸载开始顺序是确定的,但是可能交叉同时进行。而解析阶段则可以在初始化后在进行,允许Java语言运行时绑定特性。

加载

在加载阶段虚拟机需要完成以下三件事:

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

PS:
数组类型较为特殊,并不需要类加载来创建,但是它的非引用组件类型(String[][] —— String)需要类加载器来创建。

验证

准备

解析

初始化

类型初始化的场景

有且只有六种情况必须对类进行初始化(如果还没有初始化):

  • 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时。
  • 使用java.lang.reflect包下的方法对类型进行反射调用时。
  • 初始化子类时,如果父类还没初始化,那先初始化父类。
  • 虚拟机启动时,先初始化被指定的主类(包含main()函数)。
  • 当使用JDK 7新添加的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类还没有进行过初始化,则需要先触发其初始化。
  • 调用JDK 8 新添加的default方法时。

    被动引用不会导致类型初始化

以上6种场景,称为主动引用。而被动引用都不会触发类型的初始化,可见以下三个例子:

  • 子类调用父类静态字段 ```java class SuperClass { static {

    1. System.out.println("superClass init");

    }

    public static int value = 1; }

class SubClass extends SuperClass { static { System.out.println(“SuperClass init”); } }

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

  1. ```java
  2. superClass init
  3. 1

这里SubClass是被动引用,实际是引用了SuperClass的static变量。

  • 创建数组实例

    1. class Test {
    2. public static void main(String[] args) {
    3. SubClass[] arr = new SubClass[1];
    4. System.out.println(arr.getClass());
    5. }
    6. }
    1. class [Lcom.company.SubClass;

    在Java中一切皆对象,自然包括数组,虚拟机会自动生成一个SubClass数组类,创建动作由newarray字节码指令触发。此时的SubClass可以认为是一个参数类型。

  • 对常量的引用 ```java class SubClass extends SuperClass { static {

    1. System.out.println("SuperClass init");

    }

    public static final int y = 100; }

class Test { public static void main(String[] args) { System.out.println(SubClass.y); // 编译后:System.out.println(100); } }

  1. ```java
  2. 100

这与编译器对int常量的优化有关,通过编译后(字节码)‘SubClass.y’将会被‘100’替代,所以相当于直接输出100,而不再需要引用SubClass,此时SubClass与Test并不存在依赖关系。

卸载