JVM相关及其拓展(二) -- 虚拟机类加载机制

虚拟机类加载机制

1.定义

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

2.类的生命周期

类从被加载到虚拟机内存中开始,到卸载除内存为止,生命周期包括:加载(Loading)验证(Verification)准备(Preparation)解析(Resolution)初始化(Initialization)使用(Using)卸载(Unloading)。其中验证准备解析统称为连接(Linking)

class_lifecycle

加载、验证、准备、初始化和卸载阶段执行顺序为确定的,类的加载过程必须按照这个顺序开始。解析阶段不一定:在某些情况下可以在初始化阶段之后开始,为了支持Java语言的运行时绑定。
初始化阶段,虚拟机规范严格规定了有且只有5种情况必须立即对类进行“初始化”:

  • 遇到new ,getstatic,putstatic或invokestatic指令时,类没有进行初始化,则需要先触发初始化。最常见的Java代码场景:使用new关键字实例化对象、读取或设置一个类的静态字段、调用一个类的静态方法。
  • 使用java.lang.reflect进行反射调用的时候。
  • 初始化一个类时,发现父类还没有初始化,先触发父类初始化方法。
  • 虚拟机启动时,用户指定一个要执行的主类(包含main()方法的类),先初始化该类。
  • 使用动态语言支持时,若java.lang.invoke,MethodHandle实例最后解析结果为REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且该类没有进行初始化。

使用阶段

执行类的初始化,主动引用会触发类的初始化,被动引用不会触发类的初始化过程。

3.类加载的过程

类加载过程

加载

虚拟机需要完成以下3件事情:

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

并没有指定一个二进制字节流要从一个Class文件中获取

验证

(若代码被反复验证和使用过,可以使用-Xverify:none来关闭大部分的类验证措施,缩短虚拟机加载时间)

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

  1. 文件格式验证

    验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
    主要目的是保证输入的字节流能正确的解析并存储于方法区之内,格式上符合描述一个Java类信息的要求。在这个阶段字节流进入内存中的方法区后续不再操作字节流。

  2. 元数据验证

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

  3. 字节码验证

    通过数据流和控制流分析,确定程序语义是合法且符合逻辑的。对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机的安全的事件。

  4. 符号引用验证

    发生在虚拟机将符号引用转化为直接引用的时候,这个转化发生在解析阶段。对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。确保解析动作能正常执行

准备

准备阶段是正式为变量分配内存并设置类初始变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个时候进行内存分配的仅包括类变量(被static修饰的变量),不包括实例变量。
初始值通常情况下是数据类型的零值,如

1
public static int value = 123;//在准备阶段过程中初始值为0,而不是123。

特殊情况:若类字段的属性表中有ConstantValue属性,那么准备阶段value就会初始化为ConstantValue指定的值,如

1
public static final int value = 123;//在准备阶段过程中初始值为123。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析阶段中会涉及到直接引用(直接指向目标的指针、相对偏移量或者是一个能直接定位到目标的句柄),符号引用(以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可)。
解析动作主要针对类或接口(CONSTANT_Class_info)字段(CONSTANT_Fieldref_info)类方法(CONSTANT_Methodref_info)接口方法(CONSTANT_InterfaceMethodref_info)方法类型(CONSTANT_MethodType_info)方法句柄(CONSTANT_MethodHandle_info)调用点限定符(CONSTANT_InvokeDynamic_info)

初始化

初始化是类加载过程的最后一步。到这里才真正开始执行类中定义的Java程序代码
执行类构造器<clinit>()方法的过程。

  • <clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
  • <clinit>()方法与类的构造函数不同,他不需要显示的调用父类构造器,因此虚拟中第一个被执行的<clinit>()方法的类肯定是java.long.object
  • 初始化过程就是对变量进行赋值及执行静态代码块。

4.类的卸载

由JVM自带的类加载器(Bootstrap ClassLoader-根加载器、Extension ClassLoader-拓展加载器、Application ClassLoader-应用加载器)所加载的类,在虚拟机的生命周期中,始终不会被卸载。这一切是由于Java本身始终引用这些加载类,导致被加载的类对象也会一直可达。

只有由用户自定义的类加载器所加载的类是可以被卸载的。

类卸载的触发条件:

  • 该类所有的实例都已经被GC,也就是在JVM中不存在该类的任何实例
  • 加载该类的ClassLoader也被GC
  • 该类的java.lang.Class对象没有在任何地方被引用,如反射都无法访问该类

类卸载过程:

在方法区内的二进制数据会被卸载。

若卸载后还需要使用,那么就需要去重新加载,然后在Java的虚拟机堆区上重新生成实例。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!