类加载过程(二)

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

验证、准备、解析三个部分统称为连接阶段

一、验证

验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机本身的安全。

根据Java的开发理念,Class文件并不一定要求用Java源码编译而来,可以使用任何途径产生,甚至包括用16进制编辑器直接来编写产生Class文件。虚拟机如果不检查输入的字节流,很有可能因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

验证阶段是非常重要的,这个阶段是否严谨直接决定了Java虚拟机是否能承受恶意代码的攻击。根据《Java虚拟机规范》,如果验证到输入的字节流不符合Class文件约束,虚拟机就应抛出java.lang.VerifyError异常或其子类异常。验证阶段大致分为以下4个阶段的校验动作:

  • 文件格式验证(验证是否符合Class文件格式规范)
    1. 是否以魔数0xCAFEBABE开头
    2. 主、次版本号是否在当前虚拟机处理范围之内
    3. 常量池的常量中是否有不被支持的常量类型
    4. 指向常量池的各种索引值中是否有指向不存在或不符合类型的常量
    5. Class文件中各个部分及文件本身是否有被删除的或附加的其他信息
    6. ……
  • 元数据验证(对字节码描述的信息进行语义分析)
    1. 这个类是否有父类(除java.lang.Object之外,所有类都应有父类)
    2. 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
    3. 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
    4. 类中的字段、方法是否与父类产生矛盾(例如,覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同)
    5. ……
  • 字节码验证(通过数据流和控制流分析,确定程序语义是否合法)
    1. 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似“在操作数栈放置了int类型数据,使用时却按long类型载入本地变量表”的情况
    2. 保证跳转指令不会跳转到方法体以外的字节码指令上
    3. 保证方法体中的类型转换是有效的,例如“把父类对象赋值给子类数据类型,或把对象赋值给与它毫无关系的数据类型”,都是不合法的
    4. ……
  • 符号引用验证(对类自身以外(常量池中的各种符号引用)的信息进行匹配校验,本阶段发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析”中发生)
    1. 符号引用中通过字符串描述的全限定名是否能找到对应的类
    2. 在指定类中是否存在符合方法的字段描述以及简单名称所描述的方法和字段
    3. 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
    4. ……

验证阶段虽然非常重要,但并非必要,当所运行的代码都已被反复使用或验证过,那么可以考虑关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

二、准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的的内存都将在方法区中进行分配。这个阶段中,首先进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中;其次,这里所谓的初始值“通常情况”下是数据类型的零值

public static int i = 123;

变量i在准备阶段过后的初始值是0,而不是123,因为这是尚未开始任何Java方法,而赋值指令putstatic是在程序被编译之后的初始化阶段才会执行。

  • 整形零值:对应类型的0
  • 浮点型零值:对应类型的0
  • 字符型零值:’\u0000′
  • 布尔型零值:false
  • 引用类型零值:null

对于“特殊情况”,如类字段的字段属性表中存在ConstantValue属性(如,final修饰),那么在准备阶段就会被初始化为ConstantValue属性所指定的值 。

public static final int i = 123;

此时变量i将在准备阶段就被赋值为123

三、解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

1)符号引用与直接引用的区别
  • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用于虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。符号引用的字面量形式明确定义在Java虚拟机规范的Class文件中
  • 直接引用(Direct References):直接引用是可以直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接使用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中

虚拟机规范并未规定解析发生的具体时间,只要求了在执行以下16个用于操作符号引用的字节码指令之前,先对它们所使用的符号进行解析:

anewarray、checkcast、
getfield、getstatic、
instanceof、invokedynamic、invokeinterface、invokespecial、invokestatic、invokevirtual、
ldc、ldc_w、multianewarray、
new、
putfiled、pustatic

2)解析请求

对同一个符号引用进行多次解析请求是很常见的,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并标记为已解析)从而避免重复解析。无论是否执行多次解析,在同一个实体中,如果符号引用已经被成功解析过,那么后续的引用解析请求就应当一直成功;如果失败,那么其他指令对这个符号的解析请求也应当收到异常。

对于invokedynamic指令,上述规则则不成立。当碰到某个已经由invokedynamic指令触发过解析的符号引用时,并不意味着这个解析结果对于其他invokedynamic指令也同样生效。

解析动作主要针对:类或接口字段类方法接口方法方法类型方法句柄调用点限定符7类符号引用进行。其中,前4种为静态语言解析、后3种为动态语言解析.

3)静态语言解析
a)类或接口的解析

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

  • 如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在家在过程中,由于元数据验证、字节码验证的需要,有可能触发其他相关的类加载动作,例如加载这个类的父类或实现的接口。一旦这个加载过程出现了任何异常,解析过程就宣告失败
  • 如果C是一个数组类型,并且数组的元素类型为对象,也是就是N的描述符回事类似“[Ljava/lang/Integer”的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述符如前面所假设的形式,需加载的元素类型就是“java.lang.Integer”,接着由虚拟机生成一个代表此数组维度和元素的数组对象
  • 如果上面的步骤没有出现任何异常,那么C在虚拟机中时机上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具备对C的访问权限。如果发现不具备权限,将抛出java.lang.IllegalAccessError异常
b)字段的解析

要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。如果在解析这个类或接口符号引用的过程中出现了任何异常,都会导致字段符号引用解析的失败。如果解析成功完成,那将这个字段所属的类或接口用C表示,虚拟机规范要求按照如下步骤对C进行后续字段的搜索。

  • 如果C本身就包含了简单名称和字段描述都与目标相匹配的字段,则返回这个字段的直接饮用,查找结束
  • 否则,如果在C中实现了接口,将会按照继承关系从下往上递归搜索各个接口和它的父接口,如果接口中包含了简单名称和字段描述都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
  • 否则,如果C不是java.lang.Object的话,将会按照继承关系从下往上递归搜索其父类,如果在父类中包含了简单名称和字段描述都与目标相匹配的字段,则返回这个字段的直接引用,查找结束
  • 否则,查找失败,抛出java.lang.NoSuchFiledError异常

如果查找过程成功返回了引用,将会对这个字段进行权限验证,如果发现不具备对字段的访问权限,将抛出java.lang.IllegalAccessError异常

实际应用中,如果有一个同名字段同时出现在C的接口和父类中,或者同时在自己或父类的多个接口中出现,那编译期将可能拒绝编译

//接口
public interface interface01 {
    int i=0;
}
//父类
public class parent {
    int i = 0;
}
//子类
public class Sub extends parent implements interface01 {
    //public static int i = 0;
}
//测试
public class test {
    public static void main(String[] args) {
        System.out.println(Sub.i);
    }
}
我们注释掉子类中的“public static int i = 0”,
父类和实现的接口中将同时存在字段i,可以看到,此时编译器会拒绝编译
c)类方法的解析

类方法解析的第一个步骤与字段解析一样,也需要先解析出类方法表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,我们依然用C表示这个类,接下来虚拟机将会按照如下步骤进行后续的类方法搜索。

  • 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方发表中发现class_index中索引的C接口,那就直接抛出java.lang.IncompatibleClassError异常
  • 如果通过了第一步,在类C中查找是否有简单名称和描述符都与目标相匹配的方法,如果有则返回这个方法的直接引用,查找结束
  • 否则,在类C的父类中递归查找是否有简单名称和描述符都与目标项匹配的方法,如果有则放回这个方法的直接引用,查找结束
  • 否则,在类C实现的接口列表及它们的父接口之中递归查找是否有简单名称和描述符都与目标匹配的方法,如果存在匹配的方法,说明类C是一个抽象类,这时查找结束,抛出java.lang.AbstractMethodError异常
d)接口方法的解析

接口方法也需要先解析出接口方发表的class_index项中索引的方法所属的类或接口的符号引用,如果解析成功,依然用C表示这个接口,接下来虚拟机将会按照如下步骤进行后续的接口方法搜索

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

由于接口中所有的方法默认都是public的,所以不存在访问权限的问题,因此接口方法的符号解析应当不会抛出java.lang.IllegalAccessError异常

发表评论

邮箱地址不会被公开。 必填项已用*标注