类加载的全过程,加载、验证、准备、解析和初始化

加载

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

获取二进制字节流

  • 二进制来源可以有多种
    • zip:JAR、EAR、WAR
    • 网络:Applet
    • 运行时计算生成:动态代理
    • 其他文件生成::JSP
    • 数据库:中间件服务器(SAP Netweaver)把程序安装到数据库中来完成程序代码在进群间的分发
  • 非数组类的类加载器
    • 系统提供的引导类加载器
    • 自定义的类加载器(重写一个类加载器的loadClass()方法)
  • 数组类
    • 由JVM直接创建
    • 数组类的元素类型(去除所有维度的类型)最终时要靠类加载器去创建
    • 创建过程原则
      • 如果数组的组件类型(Componen Type,去除一个维度的类型)是引用类型,那就递归采用默认加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识
      • 如果数组的组件类型不是引用类型(如int[]),JVM将会把数组C标记为与引导类加载器关联
      • 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public

加载阶段完成后

  • JVM外部的二进制字节流就按照JVM所需的格式存储在方法区之中(格式自定义,JVM规范未规定)
  • 内存中实例化一个java.lang.Class类的对象,作为程序访问这些类型数据的外部接口

加载阶段与连接阶段的部分内容是交叉进行的,但开始时间是固定的先后顺序

连接(验证)

目的

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

文件格式验证

目的
  • 字节流是否符合Class文件格式的规范

    • 能够正常解析及存储在方法区内,格式上符合描述一个Java类型信息的要求
  • 能被当前版本的虚拟机处理

对象

基于二进制字节流

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

元数据验证

目的

保证其描述的信息符合Java语言规范的要求

对象

类的元数据(字节码描述的信息进行语义分析)

操作
  • 这个类是否有父类
  • 父类是否即成了不允许被继承(final)的类
  • 这个类如果不是抽象类,是否实现了父类或接口之中要求实现的所有方法
  • 类中的字段、方法是否与父类产生矛盾(覆盖父类的final字段、不合法的方法重载)

字节码验证

目的

通过数据流和控制流分析,确保语义是合法的、符合逻辑的

对象

类的方法体

操作
  • 保证任何时刻操作数栈的数据类型与指令代码序列都能配合工作

    • 操作数栈int类型,使用时却按long类型加载到本地变量表之中
  • 保证跳转指令不会跳转到方法体以外的字节码指令上

  • 保证方法体中的类型转换是有效的
    • 可以把一个子类对象赋值给父类,是安全的,反之不安全
说明

如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题;但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。通过程序去校验程序逻辑是无法做到绝对准确的。

优化

方法体的Code属性的属性表增加StackMapTable属性。

它描述了方法体中所有的基本块开始时本地变量表和操作数栈应有的状态,在字节码验证器件,就不需要推导这些状态的合法性,只需要检查表中的状态是否合法即可。

具体可参考R大的回答

符号引用验证

目的

对类自身以外的信息(常量池中的各种符号引用)进行匹配行校验,确保解析动作能正常执行,否则抛出java.lang.IncompatibleClassChangeError异常的子类

时机

将符号引用转化(解析时转化)为直接引用时

对象

符号引用

操作
  • 符号引用中通过字符串描述的全限定名是否能找到对应的类
  • 在执行类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符号引用中的类、字段、方法的访问性是否可被当前类访问
说明

可通过-Xveerify:none参数关闭大部分类验证措施,缩短类加载时间

连接(准备)

操作

  • 正式为类变量(static)分配内存(方法区)
    • 实例变量将随着对象一起分配在Java堆中
  • 设置类变量初始值(默认值,如int为0)
    • 赋值操作(putstatic指令)在程序被编译后,存放于类构造器<clinit>方法中,也就是初始化阶段

实例

1
public static int value=123;

符合上述描述,此阶段赋值为0

1
public static final int value=123;

准备阶段变量value就会被初始化为ConstatntCalue属性所指定的值(123)

连接(解析)

目的

将常量池内的符号引用替换为直接引用

符号引用Symbolic References:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。内存布局无关。引用目标不一定被加载到内存中。

直接引用Direct References:可以是直接指向目标的指针、相对偏移量或是一个能简洁定位到目标的句柄。内存布局相关。引用目标一定被加载到内存中。

时机

  • 执行用于操作符号引用的字节码指令之前,自定义实现时机
    • 类被加载器加载时
    • 一个符号引用将被使用钱
1
anewarray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、ldc、ldc_w、multianewarray、new、putfield、putstatic

说明

invokedynamic
  • 当碰到某个前面已经由invokedynamic指令触发过解析的符号引用时,并不意味着这个解析结果对其他invokedynamic指令同样生效。
  • invokedynamic指令的目的就是用于动态语言支持,其对应的引用称为“动态调用点限定符Dynamic Call Site Specifier”
    • 必须等到程序运行到这条指令时,解析动作才能进行。
其他
  • 虚拟机实现可以堆第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态),从而避免重复解析。

  • 只需要保证在同一个实体中,如果一个符号引用之前已经被成功解析过,那么后续的引用解析请求就应当一致成功;如果第一次失败了,其他指令对这个符号一用的解析请求也应该收到相同的异常。

对象

  • 类或接口CONSTANT_Class_info

  • 字段CONSTANT_fieldref_info

  • 类方法CONSTANT_Methodref_info

  • 接口方法CONSTANT_InterfaceMethodRef_info

  • 方法类型CONSTANT_MethodType_info

    • 介绍后续补充
  • 方法句柄CONSTANT_MethodHandle_info

    • 介绍后续补充
  • 调用点限定符CONSTANT_InvokeDynamic_info

    • 介绍后续补充

类或接口解析

假设当前代码所处的类为D,要把一个从未解析过的符号引用N解析为一个类或接口的C的直接引用

  • 如果C不是数组类型,虚拟机会把代表N的全限定名传递给D的类加载器去加载C类。

    • 在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作(加载其父类)
    • 如果出现问题,解析阶段将失败
  • 如果C是数组类型

    • 数组的元素类型为对象,如[Ljava/lang/Integer,需要加载的就是java.lang.Integer,虚拟机生成一个代表此数组维度和元素的数组对象
    • 如果前面步骤没有出现异常,那么C在虚拟机中时机上已经成为一个有效的类或接口,但在解析完成之间还有符号引用验证,确认D是否有权限访问C,如不具备,则抛出java.lang.IllegalAccessError异常

字段解析

查找

要解析一个未被解析过的字段符号引用,首先会解析字段所属的类或接口的符号引用,设为C,然后JVM会从这个类开始逐级往父类或接口找与这个字段相匹配的字段

  • C本身包含名称与描述符都匹配的字段,返回字段的直接引用,查找结束
  • C中实现了接口,按照继承关系从下往上搜索父接口,有则返回,查找结束
  • C有父类,按照继承关系从下往上搜索父类,有则返回,查找结束
  • 都没找到,抛出java.lang.NoSuchFieldError异常
验证权限

是否具备访问权限,不具备则抛出java.lang.IllegalAccessError异常

类方法解析

查找

解析方法所属的类或接口的符号引用C,解析成功,进行类方法的搜索

  • 如果C解析为接口,则抛出java.lang.IncompatibleClassChangeError异常
  • 在类C中找到是否有简单名称和描述符都匹配的方法,有则返回直接引用,查找结束
  • 类C的父类中递归查找,有则返回,查找结束
  • 类C实现的接口列表以及父接口之中递归查找,如果有,说明C是抽象类,直接抛出java.lang.AbstractMethodError异常
  • 都没找到,抛出java.lang.NoSuchMethodError异常
验证权限

是否具备访问权限,不具备则抛出java.lang.IllegalAccessError异常

接口方法解析

先解析出接口方法所属的类或接口的符号引用C,解析成功,进行接口方法的搜索

  • 如果C解析为类,则抛出java.lang.IncompatibleClassChangeError异常
  • 在接口C中查找,有则返回,查找结束
  • 在接口C的父接口中递归查找,直到java.lang.Object类(包含此类),有则返回,查找结束
  • 都没找到,抛出java.lang.NoSuchMethodError异常

接口方法默认为public,所以应当不会抛出java.lang.IllegalAccessError异常

初始化

最后一步,真正执行类中定义的Java程序代码,执行类构造器<clinit>()方法

<clinit>()特点

  • 由编译器自动收集类中的所有类变量的赋值动作静态语句块(static)中的语句合并产生的
    • 收集顺序:源文件中出现的顺序
    • 静态语句块中只能访问到定义在之前的变量,定义在后面的变量,在静态语句块中可以赋值,不能访问(使用)
1
2
3
4
5
6
7
8
9
public Class Test{
static{
//正常编译
i = 0;
//编译器提示:Illegal forward reference
System.out.println(i);
}
static int i = 1;
}
  • 父类中定义的静态语句块优于子类的变量赋值操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Parent {

public static int A = 1;

static {
A = 2;
}

static class Sub extends Parent {
public static int B = A;
}


public static void main(String[] args) {
//2
System.out.println(Sub.B);
}
}
  • 对于类或接口来说不是必需的

    • 没有静态语句块,也没有对变量的赋值操作,可以不生成
  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作

    • 执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法
    • 只有当父接口中定义的变量使用时,才初始化父接口
    • 接口的实现类在初始化时也不会执行接口的<clinit>()方法
  • JVM保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步

    • 只有一个线程去执行这个类的<clinit>()方法,其他线程阻塞,直至方法执行完毕
    • 如果<clinit>()方法方法中有耗时很长的操作,就可能造成多进程阻塞。