JVM的类加载

JVM的类加载

类的生命周期

类的生命周期分为以下几个阶段:加载验证准备解析初始化使用卸载。其中验证、准备、解析3个部分统称为连接(Linking)
对于初始化阶段,虚拟机严格规范了有且只有5种情况必须立即对类进行“初始化”:

  • 遇到new、getstatc、putstatic、invokestatic这4条字节码指令时,如果类还没有进行过初始化,则需要触发其初始化。
  • 使用java.lang.reflect包的方法对类进行反射调用时,如果类还没有进行过初始化,则需要触发其初始化。
  • 当初始化一个类时,如果发现其父类还未进行初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个主类。
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

加载

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

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

数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的,但是数据的元素类型(数组去掉所有维度的类型)最终是要通过类加载器去创建,一个数组类C的创建过程遵循以下规则:

  1. 如果数组的组件类型(数组去掉一个维度的类型)是引用类型,那就递归采用上面的加载过程区加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识。
  2. 如果数组的组件类型不是引用类型,Java虚拟机将会把数组C标记为与引导类加载器关联。
  3. 数组的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组的可见性将默认为public。

验证

1. 文件格式验证

目的:保证输入的字节流能正确的解析并存储于方法去中。

2. 元数据验证

目的:对类的元数据信息进行校验。

3. 字节码验证

目的:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。将对类的方法体进行校验分析。

4. 符号引用验证

目的:确保解析动作能正常执行。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段(这里所说的初始值“通常情况”下是数据类型的零值),这些类变量所使用的内存都将在方法区中分配。
这个阶段进行内存分配的仅包含类变量,不包含实例变量,实例变量将在对象实例化时随着对象一起在java堆中分配
假设一个类变量的定义如下:

public static int value = 123;

那变量value在准备阶段过后的初始值是0而不是123,因为这个时候尚未执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放在类构造器的<clinit>方法之中,所以把value赋值为123的动作将在初始化阶段执行。
通常情况下初始值是零值,还有特殊情况:如果类的字段属性表中存在ConstantValue属性,那在准备阶段该字段就会被初始化为ConstantValue属性所指定的值
假设上面类变量value的定义如下:

public static final int value = 123;

编译时`Javac将会为value生成ConstantValue属性,在准备阶段``虚拟机就会根据ConstantValue的设置将value赋值为123;

解析

类或接口的解析(CONSTANT_ Class_info)

假设当前类为D,要把一个从未解析过的符号引用N解析为一个类或接口C的直接引用,解析过程如下:

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

字段解析(CONSTANT_Fieldref_info)

  1. 要解析一个从未被解析过的字段的符号引用,首先会对字段表内class_index项索引的CONSTANT_Class_info符号引用进行解析,若解析成功,用C表示这个字段所属的类或接口。
  2. 如果C本身就包含了简单名称和字段描述符与目标相匹配的字段,则返回这个字段的直接饮用,查找结束。
  3. 否则,如果C实现了接口,则按照继承关系从下往上递归搜索各个接口和它的父接口,若接口中包含了简单名称和字段描述与目标相匹配的字段,则返回这个字段的直接引用,查找结束。
  4. 否则,如果C不是java.lang.Object类型,将会按照继承关系从下往上递归搜索其父类,若父类中包含了简单名称和字段描述与目标相匹配的字段,则返回这个字段的直接饮用,查找结束。
  5. 否则,查找失败,抛出java.lang.NoSuchFieldError异常。

如果成功查找到直接引用,会对这个字段进行权限验证,如果发现不具备对字段的访问权限,抛出java.lang.IllegalAccessError异常。
实际情况,虚拟机的编译器实现可能比上述过程更加严格:如果一个同名字段同时出现在C的父类和接口中,或同时在自己和父类的多个接口中出现,那编译器可能会拒绝编译。

类方法解析(Class_Methodref_info)

  1. 首先对方法表中class_index项索引的CONSTANT_Class_info符号引用进行解析,若解析成功,用C表示这个类。
  2. 类方法和接口方法符号引用的常量类型定义是分开的,如果在发现在方法表中的class_index字段索引的C是接口,则抛出java.lang.IncompatibleClassChangeError异常。
  3. 否则,如果C类中包含了简单名称和描述符与目标相匹配的方法,则返回这个方法的直接引用,查找结束。
  4. 否则,在C的直接父类中递归查找是否包含了简单名称和描述符与目标相匹配的方法,如果包含,则返回这个方法的直接引用,查找结束。
  5. 否则,在C实现的接口和它们的父接口中递归查找是否包含简单名称和描述符与目标相匹配的方法,如果包含,则说明C是一个抽象类,此时查找结束,抛出一个java.lang.AbstractMethodError异常。
  6. 否则,查找失败,抛出java.lang.NoSuchMethodError异常。

如果成功查找到方法的直接引用,将会对这个方法进行权限验证,如果发现不具备对这个方法的访问权限,则抛出java.lang.IllegalAccessError异常。

接口方法解析(Class_InterfaceMethodref_info)

  1. 首先对接口方法表中class_index项索引的CONSTANT_InterfaceMethodref_info符号引用进行解析,若解析成功用C表示这个接口。
  2. 如果C是个类,则直接抛出java.lang.IncompatibleClassChangeError异常。
  3. 否则,在接口C中查找是否包含简单名称和描述符都与目标相匹配的方法,如果包含,则返回这个字段的直接引用,查找结束。
  4. 否则,在接口C的父接口中递归查找,直到java.lang.Object类(包括Object类)为止,看是否有简单名称和描述符都与目标相匹配的方法,若果有,则返回这个方法的直接引用,查找结束。
  5. 否则,查找失败,抛出java.lang.NoSuchMethodError异常。

由于接口中所有方法的默认访问权限都是public的,所以不存在访问权限的问题,也就不会抛出java.lang.IllegalAccessError异常。

初始化

初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法可能会影响程序运行行为的特点和细节:

  • <clinit>()方法是由编译器自动收集类中类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是按照语句在源文件中出现的顺序决定的,静态语句块中只能访问定义在静态语句块之前的类变量。
  • <clinit>()方法不需要显示的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。
  • 接口中不能使用静态语句块,接口与类不同的是:执行接口的<clinit()>方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才会初始化。接口的实现类在进行初始化时也不会执行接口的<clinit>()方法。
  • 虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确的加锁、同步,若有多个线程同时初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动的线程执行完<clinit>()方法。

参考:

  • 《深入理解Java虚拟机(第2版)》周志明 著
  • 《Java虚拟机规范 java se7》

你可能感兴趣的:(java,jvm,类加载)