《深入理解Java虚拟机》——类加载的过程

264 阅读5分钟

java虚拟机中类加载的全过程:

image.png

加载

加载阶段,虚拟机需要完成3件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
虚拟机的这三点要求有很大的灵活性,没有具体指明从哪里获取二进制字节流,运行时数据结构和Class对象的存放位置也由虚拟机自行实现。开发人员也可以通过自定义类加载器控制类加载的行为。

验证

验证的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致会完成4个阶段的校验动作:

1)文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。该验证阶段的主要目的是保证输入的字节流能正确解析并存储于方法区内。该阶段是基于二进制字节流进行的,验证通过后,字节流会存储于方法区中。后面的几个阶段都是对方法区的存储结构进行验证。 包括:

  • 是否以魔数0xCAFEBABE开头
  • 主次版本号
  • 常量等等

2)元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求。

3)字节码验证

这是最复杂的验证阶段,主要目的是通过数据流和控制流分析,确保程序语义是合法的、符合逻辑的。这个阶段主要是对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。例如校验类型转换是否有效、跳转指令是否在范围内等。

4)符号引用验证

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

准备

准备阶段是正式为类变量分配内存并设置类变量(static修饰的变量)初始值的阶段。这些变量所使用的内存都将在方法区中进行分配。 注意:(1)准备阶段进行类型分配的仅包括类变量,而不包括实例变量,实例变量将在初始化阶段赋值并存放于堆中。(2)准备阶段只是赋予初始值,初始值一般是数据类型的零值。

public static int value = 123; // 变量value在准备阶段后的初始值是0。在初始化阶段才会赋值为123
public static final int value = 123; // 变量value在准备阶段后的初始值是123。

解析

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

  • 符号引用:
    以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。在Class文件中它以CONSTANT_Class_info、CONSTANC_Fieldred_info、CONSTANT_Methodref_info等类型的常量出现。
    符号引用的目标不一定加载到内存中了。
  • 直接引用:
    直接引用可以是直接执行目标的指针、相对偏移量、或一个能间接定位到目标的句柄。
    直接引用的目标必定已经在内存中了。 虚拟机规范中并未规定解析发生的具体时间,只要求了再执行16个用于操作符号引用的字节码指令之前,先对他们所使用的符号引用进行解析。所以虚拟机实现时可以根据需要来决定是在类加载时进行解析,还是在符号引用被使用前再解析。

初始化

类初始化阶段是类加载过程的最后一步,在初始化阶段,是执行类构造器<clinit>()的过程。

  • <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块static{}中的语句合并产生的。编译器收集的顺序是由语句在源文件中出现的顺序决定的。
  • <clinit>()方法与类的构造函数<linit>()不同,它不需要显示地调用父类构造器,虚拟机会保证在子类的<clinit>()执行之前,父类的<clinit>()已经执行完毕。
  • 接口执行<clinit>()之前,不需要保证父接口的<clinit>()已经执行。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时一样也不会执行接口的<clinit>()方法。
  • 虚拟机会保证一个雷的<clinit>()方法在多线程环境中被正确地加锁、同步。如果多个线程同时去初始化一个类,只会有一个线程去执行<clinit>()方法,其他线程都需要阻塞等待。等活动线程执行<clinit>()完毕后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个类加载器下,一个类只会初始化一次。