虚拟机类加载机制:类加载过程

本文详细解析了Java类加载过程的五个阶段:加载、验证、准备、解析和初始化。介绍了类加载器的工作原理,以及如何自定义类加载器。深入探讨了每个阶段的具体任务和作用,包括字节码验证、静态成员变量的初始化等关键环节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

类的加载过程包括:加载、验证、准备、解析和初始化。

加载

**“加载”“类加载”**的一个阶段。在加载阶段,虚拟机完成以下三件事情:

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

其中,二进制字节流可以从以下方式获取:

  • 从ZIP包读取,成为JAR,EAR,WAR格式的基础
  • 从网络中获取,如Applet
  • 运行时计算生成,如动态代理技术。在java.lang.reflect.Proxy使用ProxyGenerator.generateProxyClass的代理类的二进制字节流。
  • 由其他文件生成,例如JSP文件生成对应的Class类

非数组类的加载阶段,既可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器区完成,开发人员可以通过定义自己的类加载器区控制字节流的获取方式。

对于数组而言,数据类不通过类加载器区创建,而是有JVM直接创建的。当程序在运行过程中遇到new关键字创建一个数组,由JVM直接创建数组类,再由类加载器创建数组中的元素类。如果数组元素不是引用类型,jvm会吧数组标记为与引导类加载器关联。

注意

  1. JVM规范并未给出类在方法区中存放的数据结构。类完成加载后,二进制字节流就以特定的数据结构存储在方法区中,但存储的数据结构是由虚拟机自己定义的,JVM规范并没有指定。
  2. JVM规范并没有指定Class对象存放的位置。在二进制字节流以特定格式存储在方法区后,JVM会创建一个java.lang.Class类型的对象,作为本类的外部接口。既然是对象就应该存放在堆内存中,不过JVM规范并没有给出限制,不同的虚拟机根据自己的需求存放这个对象。HotSpot将Class对象存放在方法区。
  3. 加载阶段和连接阶段是交叉的。类加载过程中每个步骤的开始顺序都有严格限制,但每个步骤的结束顺序没有限制。也就是说,类加载过程中,必须按照如下顺序开始: 加载、连接、初始化,但结束顺序无所谓,因此由于每个步骤处理时间的长短不一就会导致有些步骤会出现交叉。

验证

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

验证阶段比较耗时,它非常重要但不一定必要,如果所运行的代码已经被反复使用和验证过,那么可以使用-Xverify:none参数关闭,以缩短类加载时间。

验证的过程包括:文件格式验证、元数据验证、字节码验证、符号引用验证。

  1. 文件格式验证

验证字节流是否符合Class文件格式的规范,并且能被当前版本虚拟机接受。

该阶段的主要目的是保证加载完成后输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这阶段的验证是基于二进制字节流进行的,只有通过这个阶段的验证之后,字节流才会进入内存的方法区中进行存储,所以后边的三个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流。

在加载开始前,二进制字节流还没进入方法区;加载完成后,二进制字节流已经存入方法区。在文件格式验证前,二进制字节流尚未进入方法区,文件格式验证通过之后才进入方法区。也就是说,加载开始后,立即启动了文件格式验证,本阶段验证通过后,二进制字节流被转换成特定数据结构存储到方法区中,继而开始下阶段的验证和创建Class对象等操作。这个过程验证了加载和验证是交叉进行的。

  1. 元数据验证 对方法区中的字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范的要求,目的是保证不存在不符合Java语言规范的元数据信息。
  2. 字节码验证 是验证过程中最复杂的一个阶段,目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。本阶段对方法体进行语义分析,保证方法在运行时不会出现危害虚拟机的事件。
  3. 符号引用验证 最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三个阶段——解析阶段中发生。符号引用验证的目的是确保解析动作能正常执行。

准备

准备阶段做两件事情:

  1. 为已经在方法区中的类中的静态成员变量分配内存,类的静态成员变量也存储在方法区中。
  2. 为静态成员变量设置初始值(0,false,null)。

假设一个类变量的定义为: public static int value = 123; 则变量value在准备阶段之后的初始值为0而不是123,把value赋值为123的动作在初始化阶段才会执行。

对于常量,如下定义的类变量: public static final int value=123; 在编译时Javac会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为123。## 解析 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

  • 符号引用:以一组符号来描述所引用的目标,符号可以是符合约定的任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能简洁定位到目标的句柄。直接引用与虚拟机实现的内存布局相关,引用的目标必定已经在内存中存在。

解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化

初始化阶段是类加载过程中的最后一步,前边的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全有虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(字节码)。

初始化阶段也即虚拟机执行类构造器()方法的过程。在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。

()方法的特点:

  • 是由编译器自动收集类中所有类变量的赋值动作和static块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。需要注意:static块只能访问到定义在它之前的类变量,定义在它之后的只能赋值不能访问。
public class Test{
    static{
        i = 0;//给变量赋值可以编译通过
        System.out.print(i);//编译器提示非法向前引用
    }
    static int i = 1;
}
复制代码
  • 该方法不需要显式调用父类的构造器。虚拟机会保证在子类的()方法执行之前,父类的该方法已经执行完毕。因此虚拟机中第一个执行 () 方法的类肯定为 java.lang.Object。
  • 父类的该方法先执行,意味着父类中定义的静态语句块的执行要优先于子类。
public class Test  {
    static class Parent {
        public static int A = 1;
        static {
            A = 2;
        }
    }

    static class Sub extends Parent {
        public static int B = A;
    }

    public static void main(String[] args) {
        System.out.println(Sub.B);  // 2
    }

}
复制代码
  • 如果一个类中不包含静态语句块,也没有对类变量赋值,编译器可以不为该类生成()方法
  • 接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生成()方法。但是执行接口的该方法不需要先执行父接口的该方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外接口实现类在初始化时也一样不会执行接口的该方法。
  • 虚拟机会保证一个类的 () 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 () 方法,其它线程都会阻塞等待,直到活动线程执行 () 方法完毕。如果在一个类的 () 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值