第7章 虚拟机类加载机制

从问卷到结论:手把手教你用SPSS 28完成一份完整的社科数据分析报告 本文详细介绍了如何使用SPSS 28完成社会科学研究中的数据分析报告,从问卷数据导入、清洗到描述性和推断性统计分析,再到结果呈现与报告撰写。通过实际案例演示,帮助研究者掌握SPSS 28的核心功能,提升数据分析效率与报告质量。 阅读详情

目录

虚拟机类加载机制

7.1 概述

7.2 类加载的时机

7.3 类加载的过程

7.3.1 加载

7.3.2 验证

7.3.3 准备

7.3.4 解析

7.3.5 初始化

7.4 类加载器

7.4.1 类与类加载器

7.4.2 双亲委派模型

7.4.3 破坏双亲委派模型


虚拟机类加载机制

7.1 概述

上一章我们了解了Class文件存储格式的具体细节,在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机后会发生什么变化?这些都是本章将要讲解的内容。

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

与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类的加载、连接和初始化过程都是在程序运行期间完成的,这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java里天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。例如,如果编写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类;用户可以通过Java预定义的和自定义类加载器,让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分,这种组装应用程序的方式目前已广泛应用于Java程序之中。从最基础的Applet、JSP到相对复杂的OSGi技术,都使用Java语言运行期类加载的特性。

为了避免语言表达中可能产生的偏差,在本章正式开始之前,笔者先设立两个语言上的约定:第一,在实际情况中,每个Class文件都有可能代表着Java语言中的一个类或接口,后文中直接对“类”的描述都包括了类和接口的可能性,而对于类和接口需要分开描述的场景会特别指明;第二,与前面介绍Class文件格式时的约定一致,笔者本章所提到的“Class文件”并非特指某个存在于具体磁盘中的文件,这里所说的“Class文件”应当是一串二进制的字节流,无论以何种形式存在都可以。

7.2 类加载的时机

注意:加载、验证、准备、解析、初始化、使用、卸载7个阶段都是针对Class文件来说的,仅仅是把Class二进制文件加载到内存中而已,还没有到实例呢。尤其是初始化,说的是对Class文件的初始化,比如类属性(static feild),类方法(static method),此时还没有到实例化对象的时候呢。

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking),这7个阶段的发生顺序如图7-1所示。

图7-1 类的生命周期

图7-1中, 加载(Loading)、验证(Verification)、准备(Preparation)、初始化(Initialization)和卸载(Unloading)这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言运行时绑定(也称为动态绑定或晚期绑定)。注意,这里笔者写的是按部就班地“开始”,而不是按部就班地“进行”或“完成”,强调这点是因为这些阶段通常都是互相交叉地混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。

什么情况下需要开始类加载过程的第一个阶段:加载?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有且只有5中情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):

1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。生成这4条指令的最常见的Java代码场景是:使用new关键字实例化对象的时候、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。

package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description:
 * @Create: 2021/2/6 下午12:27
 */
public class SuperClass {

    public static int value = 123;

    static {
        System.out.println("SuperClass static code init!");
    }


    public SuperClass() {
        System.out.println("SuperClass constructor init!");
    }

    public static void setValue(int value) {
    }

    public static int getValue() {
        return 0;
    }
}




package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description: 主动引用触发初始化,演示一
 * @Create: 2021/2/8 下午9:33
 */
public class Initialization {

    public static void main(String[] args) {

        args = new String[]{"invokestatic"};

        switch (args[0]) {
            case "new":
                // 使用new关键字实例化对象的时候
                new SuperClass();
                break;
            case "getstatic":
                // 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
                System.out.println(SuperClass.value);
                break;
            case "putstatic":
                // 读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
                SuperClass.value = 321;
                break;
            case "invokestatic":
                // 调用一个类的静态方法的时候
                if (Math.random() > 0.5) {
                    SuperClass.setValue(321);
                } else {
                    System.out.println(SuperClass.getValue());
                }
                break;
            default:
                break;
        }
    }
}

2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。(Class.forName("xxx.yyy.zzz"))

package club.limeyu.classloading;

import java.lang.reflect.InvocationTargetException;

/**
 * @Author liangmy
 * @Description: 主动引用触发初始化,演示二
 * @Create: 2021/2/8 下午9:33
 */
public class Initialization {

    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        SuperClass.class.getConstructor().newInstance();
    }
}

3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description:
 * @Create: 2021/2/6 下午8:25
 */
public class SubClass extends SuperClass {

    public static int subValue = 456;

    static {
        System.out.println("SubClass static code init!");
    }

    public SubClass() {
        System.out.println("SubClass constructor init!");
    }

    public static int getSubValue() {
        return subValue;
    }

    public static void setSubValue(int subValue) {
        SubClass.subValue = subValue;
    }
}




package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description: 主动引用触发初始化,演示三
 * @Create: 2021/2/8 下午9:33
 */
public class Initialization {

    public static void main(String[] args) {
        new SubClass();
    }
}

4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description: 主动引用触发初始化,演示四
 * @Create: 2021/2/8 下午9:33
 */
public class Initialization {

    static {
        System.out.println("main static code init");
    }
    public static void main(String[] args) {
    }
}

5)当使用JDK 1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description:
 * @Create: 2021/2/9 上午11:15
 */
public class MethodHandleClass {
    static {
        System.out.println("MethodHandleClass static code init!");
    }

    public MethodHandleClass() {
        System.out.println("MethodHandleClass constructor init!");
    }

    // REF_invokeStatic
    public static void testREF_invokeStatic(String str) {
        System.out.println(str);
    }
}




package club.limeyu.classloading;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

/**
 * @Author liangmy
 * @Description: 主动引用触发初始化,演示五
 * @Create: 2021/2/8 下午9:33
 */
public class Initialization {

    public static void main(String[] args) {
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        try {
            // REF_invokeStatic
            MethodHandle testREF_invokeStatic = lookup.findStatic(MethodHandleClass.class, "testREF_invokeStatic", MethodType.methodType(void.class, String.class));
            testREF_invokeStatic.invoke("啥也不干,打印一段话");

            // REF_getStatic

            // REF_putStatic
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

对于这5种会触发类进行初始化的场景,虚拟机规范中使用了一个很强烈的限定语:“有且只有”,这5种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的地方都不会触发初始化,称为被动引用。下面举3个例子来说明何为被动引用,分别见代码清单7-1~代码清单7-3。

package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description: 
 * 被动使用类字段演示一:
 * 通过子类引用父类的静态字段,不会导致子类初始化。
 * @Create: 2021/2/6 下午12:27
 */
public class SuperClass {

    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;
}




package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description:
 * @Create: 2021/2/6 下午8:25
 */
public class SubClass extends SuperClass {

    static {
        System.out.println("SubClass init!");
    }
}




package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description:
 * 被动使用类字段演示一:
 * 通过子类引用父类的静态字段,不会导致子类初始化。
 * @Create: 2021/2/6 下午8:26
 */
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(SubClass.value);
    }
}




Console : 
SuperClass init!
123

上述代码运行之后,只会输出“SuperClass init!”,而不会输出“SubClass init!”。对于静态字段,只有直接定义这个字段的类才会被初始化,因此通过其子类来引用父类中定义的静态字段,只会触发父类的初始化而不会触发子类的初始化。至于是否要触发子类的加载和验证,在虚拟机规范中并未明确规定,这点取决于虚拟机的具体实现。对于Sun HopSpot虚拟机来说,可通过 -XX:+TraceClassLoading 参数观察到此操作会导致子类的加载。

package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description: 
 * 被动使用类字段演示二:
 * 通过数组定义来引用类,不会触发此类的初始化。
 * @Create: 2021/2/6 下午12:27
 */
public class SuperClass {

    static {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;
}




package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description:
 * @Create: 2021/2/6 下午8:25
 */
public class SubClass extends SuperClass {

    static {
        System.out.println("SubClass init!");
    }
}




package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description:
 * 被动使用类字段演示二:
 * 通过数组定义来引用类,不会触发此类的的初始化
 * @Create: 2021/2/6 下午8:26
 */
public class NotInitialization {
    public static void main(String[] args) {
        SuperClass[] superClasses = new SuperClass[10];
    }
}




Console : 

运行之后发现没有输出“SuperClass init!”,说明并没有触发类 club.limeyu.classloading.SuperClass 的初始化阶段。但是这段代码里面触发了另外一个名为“[Lclub.limeyu.classloading.SuperClass”的类的初始化阶段,对于用户代码来说,这并不是一个合法的类名称,它是一个由虚拟机自动生成的、直接继承于 java.lang.Object 的子类,创建动作由字节码指定 newarray 触发。

这个类代表了一个元素类型为 club.limeyu.classloading.SuperClass 的一维数组,数组中应有的属性和方法(用户可直接使用的只有被修饰为 public 的 length 属性和 clone() 方法)都实现在这个类里。Java语言中对数组的访问比C/C++相对安全是因为这个类封装了数组元素的访问方法,而C/C++直接翻译为对数组指针的移动。在Java语言中,当检查到发生数组越界时会抛出 java.lang.ArrayIndexOutOfBoundsException 异常。

package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description: 
 * 被动使用类字段演示三:
 * 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
 * @Create: 2021/2/6 下午9:41
 */
public class ConstClass {

    static {
        System.out.println("ConstClass init!");
    }

    public static final String HELLO_WORLD = "hello world";
}




package club.limeyu.classloading;

/**
 * @Author liangmy
 * @Description:
 * 被动使用类字段演示三:
 * 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
 * @Create: 2021/2/6 下午8:26
 */
public class NotInitialization {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLO_WORLD);
    }
}




Console : 
hello world

上述代码运行之后,也没有输出“ConstClass init!”,这是因为虽然在Java源码中引用了 ConstClass 类中的常量 HELLO_WORLD,但其实在编译阶段通过常量传播优化,已经将此常量的值 “hello world” 存储到了NotInitialization 类的常量池中,以后 NotInitialization 对常量 ConstClass.HELLO_WORLD 的引用实际都被转化为 NotInitlization 类对自身常量池的引用了。也就是说,实际上NotInitialization 的 Class 文件之中并没有 ConstClass 类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。

注意:ConstClass.class 文件删除,执行 java club.limeyu.classloading.NotInitialization 一样会输出 hello world ,证明以上。

接口的加载过程与类加载过程稍有一些不同,针对接口需要做一些特殊说明:接口也有 初始化过程,这点与类是一致的,上面的代码都是用静态语句块“static{}”来输出初始化信息的,而接口中不能使用“static{}”语句块,但编译器仍然会为接口生成“<clinit>()”类构造器,用于初始化接口中所有定义的成员变量。接口与类真正有所区别的是前面讲述的5中“有且仅有”需要开始初始化场景中的第3种:当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。

7.3 类加载的过程

接下来我们详细讲解一下Java虚拟机中类加载的全过程,也就是加载、验证、准备、解析和初始化这5个阶段所执行的具体动作。

7.3.1 加载

“加载”是“类加载”(Class Loading)过程的一个阶段,希望读者没有混淆这两个看起来很相似的名词。在加载阶段,虚拟机需要完成以下3件事情:

1)通过一个类的权限定名来获取定义此类的二进制字节流。

2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

虚拟机规范的这3点要求其实并不算具体,因此虚拟机实现与具体应用的灵活度都是相当大的。例如“通过一个类的全限定名来获取定义此类的二进制字节流”这条,它没有指明二进制字节流要从一个Class文件中获取,准确地说是根本没有指明要从哪里获取、怎样获取。虚拟机设计团队在加载阶段搭建了一个相当开放的、广阔的“舞台”,Java发展历程中,充满创造力的开发人员则在这个“舞”上玩出了各种花样,许多举足轻重的Java技术都是建立在这一基础之上,例如:

  • 从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础。
  • 从网络中获取,这种场景最典型的应用就是Applet。
  • 运行时计算生成,这种场景使用得最多的就是动态代理技术,在java.lang.reflect.Proxy中,就是用了ProxyGenerator.generateProxyClass来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流。
  • 由其他文件生成,典型场景是JSP应用,即由JSP文件生成对应的Class类。
  • 从数据库中读取,这种场景相对少见些,例如有些中间件服务器(如SAP Netweaver)可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
  • ... ...

相对于类加载过程的其他阶段,一个非数组类的加载阶段(准确地说,是加载阶段中获取类的二进制字节流的动作)是开发人员可控性最强的,因为加载阶段既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器去完成,开发人员可以通过定义自己的类加载器去控制字节流的获取方式(即重写一个类加载器的loadClass()方法)。

对于数组类而言,情况就有所不同,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型(Element Type,指的是数组去掉所有纬度的类型)最终是要靠类加载器去创建,一个数组类(下面简称为C)创建过程就遵循以下规则:

  • 如果数组的组件类型(Component Type,指的是数组去掉一个纬度的类型)是引用类型,那就递归采用本节中定义的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识(这点很重要,在7.4节会介绍到,一个类必须与类加载器一起确定唯一性)。
  • 如果数组的组件类型不是引用类型(例如int[]数组),Java虚拟机将会把数组C标记为与引导类加载器关联。
  • 数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。

关于类加载器的话题,笔者将在本章的7.4节专门讲述。

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中,方法区中的数据存储格式由虚拟机实现自行定义,虚拟机规范为规定此区域的具体数据结构。然后在内存中实例化一个java.lang.Class类的对象(并没有明确规定是在Java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,它虽然是对象,但是存放在方法区里面),这个对象将作为程序访问方法区中的这些类型数据的外部接口。

加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序。

7.3.2 验证

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

Java语言本身是相对安全的语言(依然是相对于C/C++来说),使用纯粹的Java代码无法做到诸如访问数组边界以外的数据、将一个对象转型为它并为实现的类型、跳转到不存在的代码行之类的事情,如果这样做了,编译器将拒绝编译。但前面已经说过,Class文件并不一定要求用Java源码编译而来,可以使用任何途径产生,甚至包括用十六进制编辑器直接编写来产生Class文件。在字节码语言层面上,上述Java代码无法做到的实景都是可以实现的,至少语义上是可以表达出来的。虚拟机如果不检查输入的字节流,对其完全信任的话,很可能会因为载入了有害的字节流而导致系统崩溃,所以验证是虚拟机对自身保护的一项重要工作。

验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载子系统中又占了相当大的一部分。《Java虚拟机规范(第2版)》对这个阶段的限制、指导还是比较笼统的,规范中列举了一些Class文件格式中的静态和结构化约束,如果验证到输入的字节流不符合Class文件格式的约束,虚拟机就应抛出一个java.lang.VerifyError异常或其子类异常,但具体应当检查哪些方面,如何检查,何时检查,都没有足够具体的要求和明确的说明。直到2011年发布的《Java虚拟机规范(Java SE 7版)》,大幅增加了描述验证过程的篇幅,这时约束和验证规则才变得具体起来。受篇幅所限,本书无法逐条规则去讲解,但从整体看,验证阶段大致上会完成下面4个阶段的检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

1. 文件格式验证

第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。这一阶段可能包括下面这些验证点:

  • 是否以魔数0xCAFEBABE开头。
  • 主、次版本号是否在当前虚拟机处理范围之内。
  • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)。
  • 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量。
  • CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据。
  • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息。
  • ... ...

实际上,第一阶段的验证点还远不止这些,上面这些只是从HotSpot虚拟机源码中摘抄的一小部分内容,该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证后,字节流才会进入内存的方法区中进行存储,所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

2. 元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合Java语言规范的要求,这个阶段可能包括的验证点如下:

  • 这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类)。
  • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)。
  • 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法。
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。
  • ... ...

第二阶段的主要目的是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。

3. 字节码验证

第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。在第二阶段对元数据信息中的数据类型做完校验后,这个阶段将对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件,例如:

  • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中。
  • 保证跳转指令不会跳转到方法体以外的字节码指令上。
  • 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系,完全不相干的一个数据类型,则是危险和不合法的。
  • ... ...

如果一个类方法体的字节码没有通过字节码验证,那肯定是有问题的;但如果一个方法体通过了字节码验证,也不能说明其一定就是安全的。即时字节码验证之中进行了大量的检查,也不能保证这一点。这里涉及了离散数学中一个很著名的问题“Halting Problem”:通俗一点的说法就是,通过程序去校验程序逻辑是无法做到绝对准确的——不能通过程序准确地检查出程序是否能在有限的时间内结束运行。

由于数据流验证的高复杂性,【后边的不重要,抽空再看】

4. 符号引用验证

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

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类。
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
  • 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问。
  • ......

符号引用验证的目的是确保解析动作能正常执行,如果无法通过符号引用验证,那么将会抛出一个java.lang.IncompatibleClassChangeError异常的子类,如java.lang.IllegalAccessError、java.lang.NoSushFieldError、java.lang.NoSuchMethodError等。

对于虚拟机的类加载机制来说,验证阶段是一个非常重要的、但不是一定必要(因为对程序运行期没有影响)的阶段。如果所运行的全部代码(包括自己编写的及第三方包中的代码)都已经被反复使用和验证过,那么在实施阶段就可以考虑使用-Xverify:none参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

7.3.3 准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。这个阶段中有两个容易产生混淆的概念需要强调一下,首先,这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:

    public static int value = 123;

那变量value在准备阶段过后的初始值为0而不是123,因为这时候尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器<clinit>()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。表7-1列出了Java中所有基本数据类型的零值。

表7-1 基本数据类型的零值
数据类型零值 数据类型零值
int0 booleanfalse
long0L float0.0f
short(short)0 double0.0d
char'\u0000' referencenull
byte(byte)0   

上面提到,在“通常情况”下初始值是零值,那相对的会有一些“特殊情况”:如果类字段的字段属性表中存在ConstantValue属性,那在准备阶段变量value就会被初始化为ConstantValue属性所指定的值,假设上面类变量value的定义变为:

    public static final int value = 123;

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

7.3.4 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,符号引用在前一章讲解Class文件格式的时候已经出现过多次,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等类型的常量出现,那解析阶段中所说的直接引用与符号引用又有什么关联呢?

符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受到的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。

直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

7.3.5 初始化

7.4 类加载器

7.4.1 类与类加载器

7.4.2 双亲委派模型

7.4.3 破坏双亲委派模型

Autosar结合simulink的汽车应用层软件开发实例 本文基于汽车应用开发案例,并结合Autosar相关知识,生成具有Autosar标准的汽车应用层开发代码,以供学习和参考。以上就是本篇内容,主要是利用Simulink创建Autosar接口并进行端口映射的方法。 阅读详情

相关推荐

手把手教你用Node.js模拟大麦APP下单请求(含滑块验证处理)

本文详细解析了如何使用Node.js模拟大麦APP下单请求的全流程,包括接口逆向、滑块验证处理和签名算法逆向等关键技术。通过实战案例和代码示例,帮助开发者掌握移动端API调用的核心要素,如动态参数构造、安全验证突破等,提升自动化下单的成功率和效率。

weixin_42569329的博客 351

《深入理解JAVA虚拟机》第七章 虚拟机类加载机制

《深入理解java虚拟机 笔记》第七章 虚拟类加载机制 第七章 虚拟类加载机制   

乌鸦大大 1166

5、OpenClaw(龙虾助手)飞书机器人完整对接指南(2026最新版)

OpenClaw v2026.2.2+ 版本,防火墙内也能正常工作,是目前最稳定、最简单的企业内部AI助手部署方案。

欢迎光临~知1而N的博客 455

《深入理解Java虚拟机》第7章 虚拟机类加载机制

代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。7.1 概述上一章我们了解了Class文件存储格式的具体细节,在Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能运行和使用。而虚拟机如何加载这些Class文件?Class文件中的信息进入到虚拟机后会发生什么变化?这些都是本章将要讲解的内容。虚拟机把描述类的数据从Class文件加载到内存,并对数据

Watson的博客 1217

EPSON RX8010SJ RTC实战:I2C寄存器配置避坑指南(附完整初始化代码)

本文详细解析了EPSON RX8010SJ RTC芯片在I2C寄存器配置中的常见问题与解决方案,包括TEST位、STOP位和中断控制等关键配置要点。通过实战经验分享和完整的初始化代码模板,帮助开发者避免常见陷阱,确保RTC模块的稳定运行。

weixin_28678831的博客 404

uniapp+ucharts折线图toolTip不显示?别慌,手把手教你用showToolTip方法精准修复

本文详细解析了uniapp+ucharts折线图中toolTip不显示的常见问题,提供了showToolTip方法的精准修复方案。从底层原理到实战代码,帮助开发者解决触摸事件冲突、坐标计算偏差等问题,确保折线图交互提示功能稳定运行。

weixin_30316097的博客 401

【Java虚拟机】《深入理解Java虚拟机》| 虚拟机类加载机制(一)

虚拟机类加载机制 前提概念 类加载的时机 类加载的过程 类加载器 前提概念 什么是虚拟机类加载机制? 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行效验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 Java天生可以动态扩展的语言特性 与那些在编译时需要进行连接工作的语言不同,在Java语言里面,类型的加载、连接和初...

长路漫漫的歇脚处 1152

时间加强控制图,累计和控制图详细介绍,公式,计算 UCL,LCL,CL, sigma,C#实现

\(k\):参考值(通常取为检测偏移量的一半,例如 \(k = \frac{\delta \sigma}{2}\),\(\delta\)是希望检测的偏移量,\(\sigma\)是过程标准差)。- 示例中假设目标均值 \(\mu_0 = 10.0\),标准差 \(\sigma = 0.5\),参考值 \(k = 0.25\)(检测1σ偏移),控制限常数 \(h = 4\)。例如,若希望检测1个标准差的偏移(\(\delta = 1\)),则 \(k = 0.5\sigma\)。

专注于前沿技术的探索与实践,热衷于从代码逻辑、架构设计到行业应用等多维度深挖技术本质。期待在技术之路上与你共同成长~ 807

JVM 第7章 虚拟机类加载机制

这一章主要讲了2部分内容: 其一是:类加载的整个过程 其二,几种类加载器及其工作原理 1 综述 类加载机制:概括的说就是,虚拟机将class文件中描述类的数据加载到内存当中,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的java类型。 java中,类型的加载、连接和初始化过程都是在程序运行期间完成的。java的可动态扩展的特性就是依赖运行期动态加载和动态连接这个特点的实现...

try to do 242

Open3D 使用CPD算法对点云配准【2025最新版】

Coherent Point Drift相干点漂移算法实现点云配准(python代码)博客长期更新,本文最近更新时间为:2025年2月2日。

点云侠的博客 8万+

层层剖析,让你彻底搞懂Self-Attention、MultiHead-Attention和Masked-Attention的机制和原理

本文内容 本文基于李宏毅老师对 Self-Attention 的讲解,进行理解和补充,并结合Pytorch代码,最终目的是使得自己和各位读者更好的理解Self-Attention 李宏毅Self-Attention链接: https://www.youtube.com/watch?v=hYdO9CscNes PPT链接见视频下方 通过本文的阅读,你可以获得以下知识: 什么是Self-Attention,为什么要用Self-Attention? Self-Attention是如何做的? Self-Atten

iioSnail的博客 2万+

第7章 虚拟机类加载机制

第7章 虚拟机类加载机制 这一章花费了我好久的时间才整理出来的,看书和看视频结合起来一起整理,所以内容和干货有点多,如果有不正确的地方欢迎各位指正! ...

小咸白鱼的博客 516

30天入门 SAP BTP 开发系列之一:SAP 开发顾问为什么要学习 SAP BTP 开发?能带来哪些收获?

国内 SAP 开发顾问们的微信朋友圈,最近可能都被 SAP 和阿里这两家巨头宣布合作的重磅消息刷屏了吧。近日,德国软件巨头SAP SE与中国电商巨头阿里巴巴集团宣布建立战略合作伙伴关系。根据协议,阿里巴巴集团将利用 SAP Cloud ERP Private 强化企业基础设施,并采用联合上市策略为全球客户创造卓越价值。阿里巴巴将集成 SAP Business Suite,以简化系统维护与升级流程,并实现 AI 功能与核心业务流程的无缝整合。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 2365

第7章 虚拟机类加载机制

第7章 虚拟机类加载机制 Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。 与那些在编译时需要进行连接的语言不同,在Java语言里面,类型的加载,连接和初始化过程都是在程序运行期间完成的。 7.2 类加载的时机 一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历加载,验证,准备,解析,初始化,使用和卸载七个阶段,其中验证,准备,解析三个部分统称为连接。

asertgh123的博客 110

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

[深度理解java虚拟机]第7章 虚拟机类加载机制

文章目录7.1 概述7.2 类加载的时机7.3 类加载的过程7.3.1 加载7.3.2 验证7.3.3 准备7.3.4 解析7.3.5 初始化7.4 类加载器7.4.1 类与类加载器7.4.2 双亲委派模型 7.1 概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。 7.2 类加载的时...

qq_43115606的博客 2808

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

第7章-虚拟机类加载机制

文章目录7.1 概述7.2 类加载的时机7.2.1 生命周期7.2.2 加载时间点7.3 类加载的过程7.3.1 加载7.3.2 验证7.3.2.1 文件格式验证7.3.2.2 元数据验证7.3.2.3 字节码验证7.3.2.4 符号引用验证7.3.3 准备7.3.4 解析7.3.4.1 类或接口的解析7.3.4.2 字段解析7.3.4.3 类方法解析7.3.4.4 接口方法解析7.3.5 初始化...

Serendipity的博客 338

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

第7章 虚拟机类加载机制(简述)

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

m0_64421869的博客 567

深入理解Java虚拟机,第7章 虚拟机类加载机制

1、堆(Heap)和栈(Stack) Person p = new Person(); 这行代码实际产生了两个东西:一个是p变量,一个是 Person 对象。Person对象被赋给p变量。 类是一种引用数据类型,因此程序中定义的 Person 类型的变量 p 实际上是一个引用,它被存放在栈(Stack)内存里,指向实际的Person对象;而真正的Person对象则存放在堆(Heap)内存中。 当一个对象被创建成功后(new Person()产生的内容),这个对象将保存在堆内存中,Java程...

wang_yong_zhang的博客 203

深入理解Java虚拟机——第7章 虚拟机类加载机制

第7章 虚拟机类加载机制第7章 虚拟机类加载机制7.1 概述7.2 类加载的时机7.3 类加载的过程7.3.1 加载7.3.2 验证7.3.3 准备7.3.4 解析7.3.5 初始化7.4 类加载器7.4.1 类与类加载器7.4.2 双亲委派模型7.4.3 破坏双亲委派模型7.5 Java 模块化系统7.5.1 模块的兼容性7.5.2 模块化下的类加载器 第7章 虚拟机类加载机制 7.1 概述 7.2 类加载的时机 7.3 类加载的过程 7.3.1 加载 7.3.2 验证 7.3.3 准备 7.3.4 解析

zaker123的博客 394

深入理解java虚拟机 第7章 虚拟机类加载机制

目录7.1 概述7.2 类加载的时机类加载生命周期开始顺序类初始化时机主动引用被动引用接口加载与类加载过程的区别7.3 类加载的过程7.3.1 加载7.3.2 验证7.3.3 准备7.3.4解析7.3.5 初始化7.4 类加载器7.4.1 类与类加载器7.4.2 双亲委派模型工作过程好处实现自定义类加载器实现 代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大...

qq_41686190的博客 589
上一篇: RabbitMQ实战指南 - 笔记
下一篇: 第2章 Java内存区域与内存溢出异常
limeOracle
博客等级 码龄10年 11粉丝 566原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值