JVM进阶之类加载过程详解(上篇)

Posted ProChick

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM进阶之类加载过程详解(上篇)相关的知识,希望对你有一定的参考价值。

一、类的生命周期

  • 在Java中数据类型分为基本数据类型和引用数据类型,其中基本数据类型是由虚拟机预先定义的,而引用数据类型则需要进行类的加载。

  • 依据Java虚拟机规范,从字节码文件到加载到内存中的类,再到类卸载出内存,它的整个生命周期大致有七个阶段:

  • 从一个类的加载和使用过程示例上来看

二、类的加载阶段

  • 所谓加载,就是将Java类的字节码文件加载到机器内存中,并在内存中构建出一个该类的原型,也叫作类模板对象。

  • 所谓类模板对象,其实就是Java类在JVM内存中的一个快照,JVM会将由字节码文件中解析出的常量池、类字段、类方法等信息存储到类模板中,这样JVM在运行期间就能够通过类模板获取Java类中的相关信息,就能够完成对方法的调用。常说的反射机制就是建立在类模板信息的基础上进行的,如果JVM没有将Java类的相关信息存储到类模板中,那么也就无法利用反射获取类的相关信息了。

  • 在加载阶段,其实就是查找并加载类的二进制数据,然后生成该类的实例,一般有以下过程:

    • 通过全类名,获取指定类的二进制数据流。
    • 解析该类的二进制数据流,生成方法区内的数据结构。当然,如果输入的二进制数据流不满足ClassFile文件指定的结构,则会抛出ClassFormatError异常。
    • 在获取到类的二进制信息后,Java虚拟机会处理这些数据并创建Class类的实例,用以表示该类型,作为方法区中该类各种数据的访问入口。
  • 对于类的二进制数据流,虚拟机可以通过多种途径产生或获得,比较常见的有:

    • 通过文件系统读取一个字节码文件
    • 读入jar、zip等归档数据包,提取类文件
    • 读取事先存放在数据库中的类的二进制数据
    • 通过网络传输读取类的二进制数据流
    • 在运行时生成一段类的二进制信息
  • 加载的类在JVM中首先创建相应的类结构,然后将类结构存储在方法区。与此同时,还会在堆空间中创建一个Class对象,用来封装类位于方法区内的数据结构。

  • Class类的构造方法是私有的,只有JVM能够创建。 java.lang.Class的实例是访问类型元数据的接口,也是实现反射的关键入口。通过Class类提供的接口,可以获得目标类所关联的class文件中具体的数据结构信息。

    public static void main(String[] args) {
        try {
            Class clazz = Class.forName("java.lang.String");
            
            // 获取当前运行时类声明的所有方法
            Method[] ms = clazz.getDeclaredMethods();
            for (Method m : ms) {
                // 获取方法的修饰符
                String mod = Modifier.toString(m.getModifiers());
                System.out.print(mod + " ");
                
                // 获取方法的返回值类型
                String returnType = m.getReturnType().getSimpleName();
                System.out.print(returnType + " ");
                
                // 获取方法的名称
                System.out.print(m.getName() + "(");
                
                // 获取方法的参数列表
                Class<?>[] ps = m.getParameterTypes();
                if (ps.length == 0) System.out.print(')');
                for (int i = 0; i < ps.length; i++) {
                    char end = (i == ps.length - 1) ? ')' : ',';
                    System.out.print(ps[i].getSimpleName() + end);
                }
                System.out.println();
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
    
  • 💡【注】:如果数组的元素类型是基本数据类型,由于已在Java虚拟机中预先定义好了,所以不做处理。如果数组的元素类型是引用类型,那么就要遵循定义的加载过程,递归加载和创建数组A的元素类型。

三、类的链接阶段

🎈类的链接阶段又可以分为下面三小步

1.进行验证(Verification)

👉验证是链接操作的第一步,目的是保证加载的字节码是合法的、合理的并符合规范的。该阶段的验证虽然拖慢了加载的整体速度,但是它避免了在字节码运行时还需要进行各种检查,验证的内容如下图:

  • 格式检查
    • 字节码文件是否以魔数0xCAFEBABE开头,主版本号和副版本号是否在当前Java虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等。
    • 格式检查其实是和加载阶段一起执行的,因为只有格式验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中,而格式检查之外的验证操作将会在方法区中进行。
  • 语义检查
    • 是否所有的类都有父类的存在(除了0bject外)
    • 是否一些被定义为final的方法或者类被重写或继承了
    • 非抽象类是否实现了所有抽象方法或者接口方法
    • 是否存在不兼容的方法
  • 字节码验证
    • 主要通过对字节码流的分析,判断字节码是否可以被正确地执行,比如:在字节码的执行过程中,是否会跳转到一条不存在的指令、函数的调用是否传递了正确类型的参数、变量的赋值是不是给了正确的数据类型等。
    • 栈映射帧就是在这个阶段,用于检测在特定的字节码处,其局部变量表和操作数栈是否有着正确的数据类型。但遗憾的是,100%准确地判断一段字节码是否可以被安全执行是无法实现的,因此,该过程只是尽可能地检查出可以预知的明显的问题。如果在这个阶段无法通过检查,虚拟机也不会正确装载这个类。但是,如果通过了这个阶段的检查,也不能说明这个类是完全没有问题的。
  • 符号引用验证
    • Class文件在其常量池中会通过字符串记录自己将要使用的其他类或者方法。
    • 在验证阶段,虚拟机就会检查这些类或者方法是否存在,如果一个需要使用类无法在系统中找到,则会抛出NoClassDefFoundError异常,如果一个方法无法被找到,则会抛出NoSuchMethodError异常。

2.进行准备(Preparation)

👉准备是链接操作的第二步,目的是为类的静态变量分配内存,并将其初始化为默认值。主要数据类型的默认值如下图:

  • 这里不包含用static final共同修饰的情况,因为如果是常量的话,在编译的时候就会分配内存空间了,准备阶段只会显式赋值。
  • 这里只针对静态类变量或者静态代码块的分配初始化,非静态类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
  • 在这个阶段中只可能存在静态变量的初始化,不会涉及代码的执行。

示例

public class Test{
    private static long id;
    private static final int num = 1;
    public static finnal String constStr = "CONST";
}

3.进行解析(Resolution)

👉解析是链接操作的第三步,目的是将类、接口、字段和方法的符号引用转为直接引用。

  • 所谓解析就是将符号引用转为直接引用,也就是得到类、字段、方法在内存中的指针或者偏移量。因此,如果直接引用存在,那么可以肯定系统中存在该类、方法或者字段。但如果只存在符号引用,则不能确定系统中一定存在该类的真实结构。
  • 符号引用就是一些字面量,与Java虚拟机的内部数据结构和内存布局无关。通常在字节码文件中,通过常量池进行了大量的符号引用。但是在程序实际运行中,只有符号引用是不够的,我们必须将其转化为直接引用,这样系统才能找到该方法真正的内存地址。
  • 以方法为例,Java虚拟机会为每个类都准备一张方法表,然后将其所有的方法都列在表中,当需要调用一个类的方法的时候,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。这一过程主要就是通过解析操作,让符号引用转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。

💡【注意】: Java虚拟机规范并没有明确要求解析阶段一定要按照顺序执行,在HotSpot VM中,链接阶段中的解析操作往往会伴随着JVM在执行完初始化之后再执行。

以上是关于JVM进阶之类加载过程详解(上篇)的主要内容,如果未能解决你的问题,请参考以下文章

JVM进阶之类加载过程详解(下篇)

JVM进阶之类加载过程详解(下篇)

JVM进阶之类加载器详解

JVM进阶之类加载器详解

JVM进阶之字节码指令解析(上篇)

JVM集合之类加载子系统