# 概述

  • 虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验、 转换解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。
  • 类型的加载、 连接和初始化过程都是在程序运行期间完成的。

# 类加载的时机

  • 类的生命周期
    类的声明周期
  • 加载、 验证、 准备、 初始化和卸载这 5 个阶段的顺序是固定的,类的加载过程必须按照这种顺序按部就班地开始。(这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用、 激活另外一个阶段。)
  • 为了支持 Java 语言的运行时绑定(也称为动态绑定或晚期绑定)解析阶段在某些情况下可以在初始化阶段之后再开始。
  • 第一阶段类加载由虚拟机的具体实现来自由把握。

# 类的初始化条件(加载、验证、准备需要在此之前开始)

# 主动引用(类初始化阶段的 5 中情况)

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

# 被动引用(不会触发初始化)

  • 对于静态字段,只有直接定义这个字段的类才会被初始化。比如:通过子类引用父类中定义的静态字段时,只会触发父类的初始化而不会触发子类的初始化。是否要触发子类的加载和验证,取决于虚拟机的具体实现(对于 Sun HotSpot 虚拟机来说,可通过 - XX:+TraceClassLoading 参数观察到此操作会导致子类的加载。)
  • 通过数组定义来引用类,不会触发此类的初始化。(虚拟机会自动生成、直接继承于 java.lang.Object 的子类,创建动作由字节码指令 newarray 触发,代表一个元素类型为引用类的一维数组,并且封装了数组应有的属性和方法。封装在数组访问的 xaload、xastore 字节码指令中。)
  • 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

# 接口的初始化过程

  • 编译器仍然会为接口生成 “<clinit>()” 类构造器,用于初始化接口中所定义的成员变量。
  • 一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

# 类加载过程

# 加载

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

# 非数组类

  • 一个非数组类的加载阶段(加载阶段中获取类二进制字节流的动作)可以使用系统提供的引导类加载器来完成,也可以自定义类加载器(重写 loadClass () 方法)控制字节流的获取方式。

# 数组类

  • 数组类本身不通过类加载器而是由虚拟机直接创建,但数组类的元素类型靠类加载器去创建。
  • 如果数组的组件类型是引用类型,就递归采用加载过程去加载这个组件类型,数组 C 将在加载该组件类型的类加载器的类名称空间上被标识。
  • 如果数组的组件类型不是引用类型(例如 int [] 数组),Java 虚拟机将会把数组 C 标记为与引导类加载器关联。
  • 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。

# 连接阶段

# 验证(非必要阶段)

  • 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  • 如果所运行的全部代码(包括自己编写的及第三方包中的代码)都已经被反复使用和验证过,那么在实施阶段就可以考虑使用 - Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
# 文件格式验证

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

几种可能包括的验证点:

  • 是否以魔数 0xCAFEBABE 开头。
  • 主、 次版本号是否在当前虚拟机处理范围之内。
  • 常量池的常量中是否有不被支持的常量类型(检查常量 tag 标志)。
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据。
  • Class 文件中各个部分及文件本身是否有被删除的或附加的其他信息。
  • ......

格式验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的 3 个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

# 元数据验证

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

可能包括的验证点如下:

  • 这个类是否有父类(除了 java.lang.Object 之外,所有的类都应当有父类)。
  • 这个类的父类是否继承了不允许被继承的类(被 final 修饰的类)。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • 类中的字段、 方法是否与父类产生矛盾(例如覆盖了父类的 final 字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
  • ......
# 字节码验证

通过数据流和控制流分析,确定程序语义是合法的、 符合逻辑的,不会做出危害虚拟机安全的事件

例如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
  • 保证跳转指令不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换是有效的。
  • ......

如果一个方法体通过了字节码验证,也不能说明其一定就是安全的不能通过程序准确地检查出程序是否能在有限的时间之内结束运行

JDK 1.6 之后编译器在进行编译时给方法体的 Code 属性添加了 StackMapTable 的属性,描述了方法体中所有的基本块开始时本地变量表和操作栈应有的状态,这样在字节码验证期间,就不需要根据程序推导这些状态的合法性,只需要检查 StackMapTable 属性中的记录是否合法即可。

# 符号引用验证

阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段 —— 解析阶段中发生可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验

通常校验以下内容:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
  • 符号引用中的类、 字段、 方法的访问性(private、 protected、 public、 default)是否可被当前类访问。
  • ......

符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个 java.lang.IncompatibleClassChangeError 异常的子类,如 java.lang.IllegalAccessError、 java.lang.NoSuchFieldError、 java.lang.NoSuchMethodError 等。

# 准备

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

  • 进行内存分配的仅包括类变量(被 static 修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在 Java 堆中。
  • 初始值 “通常情况” 下是数据类型的零值
  • 基本数据类型零值
  • 如果类字段的字段属性表中存在 ConstantValue 属性,那在准备阶段变量 value 就会被初始化为 ConstantValue 属性所指定的值。

# 解析

  • 虚拟机将常量池内的符号引用替换为直接引用的过程。

符号引用与直接引用

  • 符号引用以一组符号来描述所引用的目标,可以是任何形式的字面量,与虚拟机实现的内存布局无关,引用的目标在内存中不一定存在
  • 直接引用可以是直接指向目标的指针相对偏移量或是一个能间接定位到目标的句柄,和虚拟机实现的内存布局相关的,引用的目标在内存中必定存在
  • 虚拟机规范要求在执行 anewarray、checkcast、 getfield、 getstatic、 instanceof、 invokedynamic、 invokeinterface、 invokespecial、invokestatic、 invokevirtual、 ldc、 ldc_w、 multianewarray、 new、 putfield 和 putstatic 这 16 个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。
  • 除 invokedynamic 指令以外,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作重复进行。

invokedynamic 指令用于动态语言支持,它所对应的引用称为动态调用点限定符:必须等到程序实际运行到这条指令的时候,解析动作才能进行。

  • 解析动作主要针对类或接口(CONSTANT_Class_info)、 字段(CONSTANT_Fieldref_info)、 类方法(CONSTANT_Methodref_info)、 接口方法(CONSTANT_InterfaceMethodref_info)、 方法类型(CONSTANT_MethodType_info)、 方法句柄(CONSTANT_MethodHandle_info)和调用点限定符(CONSTANT_InvokeDynamic_info)7 类符号引用进行。
# 类或接口的解析
  • 假设当前代码所处的类为 D,如果要把一个从未解析过的符号引用 N 解析为一个类或接口 C 的直接引用,解析过程需要以下 3 步:
  • 如果 C 不是一个数组类型,那虚拟机将会把代表 N 的全限定名传递给 D 的类加载器去加载这个类 C。
  • 如果 C 是一个数组类型,并且数组的元素类型为对象(N 的描述符会是类似 “[Ljava/lang/Integer” 的形式),将会按照上步规则加载数组元素类型,接着由虚拟机生成一个代表此数组维度和元素的数组对象。
  • 上述两步顺利完成后,还要进行符号引用验证,确认 D 是否具备对 C 的访问权限。(可能抛出 java.lang.IllegalAccessError 异常)
# 字段解析

对一个未被解析过的字段符号引用

  • 首先对字段表内 class_index 项中索引的 CONSTANT_Class_info 符号引用进行解析(即对该字段所属类、接口的符号引用解析)。
  • 上步解析完成后,将对字段所在的类(C)进行后续字段查找:
  • 如果 C 本身就包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  • 否则,如果在 C 中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  • 否则,如果 C 不是 java.lang.Object 的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述符都与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  • 否则,查找失败抛出 java.lang.NoSuchFieldError 异常。
# 类方法解析
  • 先解析出类方法表的 class_index 项中索引的方法所属的类或接口的符号引用。

上步解析成功后,按照以下步骤查找:(C)

  • 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中发现 class_index 中索引的 C 是个接口,那就直接抛出 java.lang.IncompatibleClassChangeError 异常。
  • 如果通过了第 1 步,在类 C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  • 否则,在类 C 的父类中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  • 否则,在类 C 实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标相匹配的方法,如果存在匹配的方法,说明类 C 是一个抽象类,这时查找结束,抛出 java.lang.AbstractMethodError 异常。
  • 否则,宣告方法查找失败,抛出 java.lang.NoSuchMethodError。
  • 如果查找过程成功返回了直接引用,将会对这个方法进行权限验证,如果发现不具备对此方法的访问权限,将抛出 java.lang.IllegalAccessError 异常。
# 接口方法解析
  • 先解析出接口方法表的 class_index 项中索引的方法所属的类或接口的符号引用。

上步解析成功后,按照以下步骤查找:(C)

  • 如果在接口方法表中发现 class_index 中的索引 C 是个类而不是接口,那就直接抛出 java.lang.IncompatibleClassChangeError 异常。
  • 否则,在接口 C 中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  • 否则,在接口 C 的父接口中递归查找,直到 java.lang.Object 类(查找范围会包括 Object 类)为止,看是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束。
  • 否则,宣告方法查找失败抛出 java.lang.NoSuchMethodError 异常。

接口方法(默认都是 public)符号解析过程不会抛出 java.lang.IllegalAccessError 异常。

# 初始化

  • 真正开始执行类中定义的 Java 程序代码(字节码)。
  • 执行类构造器() 方法的过程。
  • () 由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static {} 块)中的语句合并产生的(按照语句在源文件中出现的顺序)。

静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问(非法向前引用变量)。

  • 虚拟机会保证在子类的() 方法执行之前,父类的() 方法已经执行完毕。因此在虚拟机中第一个被执行的() 方法的类肯定是 java.lang.Object。
  • 父类中定义的静态语句块要优先于子类的变量赋值操作。
  • 如果类中没有静态语句块,编译器不会生成()。
  • 接口中可以有变量初始化的赋值操作,但执行接口的() 不需要先执行父接口的(),只有当父接口中定义的变量使用时,父接口才会初始化。并且该接口的实现类初始化时也不会执行接口的()。
  • 一个类的() 方法在多线程环境中被正确地加锁、同步(同时只会有一个线程去执行这个类的<clinit>()方法,其他线程阻塞)。
  • 如果执行() 方法的那条线程退出() 方法后,其他线程唤醒之后不会再次进入() 方法。 同一个类加载器下,一个类型只会初始化一次