JVM类加载

类加载的过程

1. 什么是类加载?

  • 简单来说,类加载 是指 Java 虚拟机(JVM)将类的 .class 文件(二进制数据)读入内存,并将其转换为 JVM 可以使用的 Class 对象的过程。这个 Class 对象是 Java 反射机制的基石,也是我们在程序中使用这个类的模板。

  • 类加载并不仅仅指"加载"这一个动作,它包含了从查找字节码到类完全可用的一整个生命周期


2. 类加载的过程(类的生命周期)
类加载的过程 主要分为以下三个核心阶段:加载、链接、初始化 。其中链接又细分为三个子阶段。

阶段一:加载
"加载"(Loading) 阶段是整个"类加载"(Class Loading)过程中的⼀个阶段,它和类加载Class Loading 是不同的,⼀个是加载 Loading 另⼀个是类加载 Class Loading,所以不要把⼆者搞混了。

此阶段主要完成三件事:

  • 通过类的全限定名获取其定义的二进制字节流。

    • 这不仅仅是从文件系统读取 .class 文件,还可以从 JAR、WAR 包、网络、数据库、运行时计算生成(动态代理)等多种来源获取。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    • 方法区(在 HotSpot JVM 8+ 中称为 Metaspace)存储了类的结构信息,如常量池、字段描述、方法描述等。
  • 在堆内存中生成一个代表这个类的 java.lang.Class 对象。

    • 这个 Class 对象作为方法区中该类各种数据的访问入口。我们通过 MyClass.class 或 obj.getClass() 获取的就是这个对象。

注意: 数组类本身不由类加载器创建,而是由 JVM 直接在内存中动态构造。但数组的元素类型(Component Type)最终还是要靠类加载器来加载
阶段二:链接(Linking)
(1)验证(Verification)
验证连接阶段的第⼀步,这⼀阶段的⽬的是确保Class⽂件的字节 流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信 息被当作代码运⾏后不会危害虚拟机⾃⾝的安全。

  • 目的: 确保被加载的类的字节流符合 JVM 规范,不会危害 JVM 的安全。

  • 主要检查:

    • 文件格式验证: 魔数(CAFE BABE)、版本号等。
    • 元数据验证: 类是否有父类(除了 Object)、是否是最终类被继承、是否实现了抽象方法等。
    • 字节码验证: 确保方法体中的代码逻辑正确(如类型转换安全)。
    • 符号引用验证: 发生在解析阶段,确保符号引用能够被正确解析。

(2)准备(Preparation)

  • 目的: 为类的静态变量(static variables) 分配内存并设置初始值。

  • 关键点:

    • 分配内存的仅包括类变量(被 static 修饰的变量),不包括实例变量。

    • 设置的是数据类型的零值 ,而不是代码中显式赋予的值。
      例如: public static int value = 123; 在准备阶段后,value 的初始值是 0,而不是 123。赋值为 123 的动作将在后面的初始化阶段执行。

    • 对于 static final 修饰的常量(ConstantValue),如果它的类型是基本类型或 String,并且在编译期就能确定值,那么准备阶段就会直接将其初始化为指定的值。例如:public static final int value = 123; 在准备阶段后,value 的值就是 123。

(3)解析(Resolution)

  • 目的: 将常量池内的符号引用(Symbolic References) 替换为直接引用(Direct References)

    • 符号引用: 一组用来描述所引用目标的符号,可以是任何形式的字面量,与虚拟机内存布局无关。

    • 直接引用: 可以是直接指向目标的指针、相对偏移量或一个能间接定位到目标的句柄。与虚拟机内存布局相关。

  • 解析的目标通常包括: 类或接口、字段、类方法、接口方法、方法类型等。

阶段三:初始化(Initialization)

  • 目的: 执行类的构造器 < clinit >() 方法的过程,真正开始执行类中定义的 Java 程序代码。

  • < clinit >() 方法是什么?

    • 它是由编译器自动收集类中的所有类变量的赋值动作和静态代码块(static{}块) 中的语句合并产生的。
    • 顺序由语句在源文件中出现的顺序决定。
  • 初始化的时机(主动引用,触发初始化):

    • 遇到 new, getstatic, putstatic, invokestatic 这四条字节码指令时。
      对应代码场景: 使用 new 关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)、调用一个类的静态方法。

    • 使用 java.lang.reflect 包的方法对类进行反射调用的时候。

    • 当初始化一个类时,如果其父类还没有被初始化,则需要先触发其父类的初始化。

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

  • 不会导致初始化的情况(被动引用):

    • 通过子类引用父类的静态字段,不会导致子类初始化。
    • 通过数组定义来引用类,不会触发此类的初始化。如 MyClass[] arr = new MyClass[10];
    • 引用一个类的常量(static final)不会触发初始化,因为常量在编译阶段就存入调用类的常量池了。

3. 类加载器(ClassLoader)

类加载器是实际执行"加载"阶段动作的组件。

三层类加载器模型( parental delegation model, 双亲委派模型)
Java 保留了三种基本的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):

    • C++ 实现,是 JVM 自身的一部分。
    • 负责加载 <JAVA_HOME>/lib 目录下的核心类库(如 rt.jar, charsets.jar 等),或者被 -Xbootclasspath 参数指定的路径中的类。
    • 无法被 Java 程序直接引用。
  • 扩展类加载器(Extension ClassLoader):

    • Java 实现,是 sun.misc.Launcher$ExtClassLoader 类。
    • 负责加载 <JAVA_HOME>/lib/ext 目录下的,或者被 java.ext.dirs 系统变量所指定的路径中的所有类库。
    • 开发者可以直接使用。
  • 应用程序类加载器(Application ClassLoader):

    • Java 实现,是 sun.misc.Launcher$AppClassLoader 类。
    • 也叫系统类加载器(System ClassLoader)。
    • 负责加载用户类路径(ClassPath)上所指定的类库。
    • 是程序中默认的类加载器。如果没有自定义类加载器,ClassLoader.getSystemClassLoader() 返回的就是它。

双亲委派模型

1. 什么是双亲委派模型?

如果⼀个类加载器收到了类加载的请求,它⾸先不会 ⾃⼰去尝试加载这个类,⽽是把这个请求委派给⽗类加载器 去完成,每⼀个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当⽗加载器反馈⾃⼰⽆ 法完成这个加载请求(它的搜索范围中没有找到所需的类)时,⼦加载器才会尝试⾃⼰去完成加载。

  • 从下到上检查: Application ClassLoader -> Extension ClassLoader -> Bootstrap ClassLoader。

  • 从上到下尝试加载: 只有当父加载器反馈自己无法完成这个加载请求(在自己的搜索范围没找到所需的类)时,子加载器才会尝试自己去加载。

  • 启动类加载器: 加载 JDK 中 lib ⽬录中 Java 的核⼼类库,即$JAVA_HOME/lib⽬录。 扩展类加载器。加载 lib/ext ⽬录下的类。

  • 应⽤程序类加载器: 加载我们写的应⽤程序。

  • ⾃定义类加载器: 根据⾃⼰的需求定制类加载器。


2.优点:

  • 避免类的重复加载: 确保一个类在 JVM 中全局唯一。⽐如 A 类和 B 类都有⼀个⽗类 C 类,那么当 A 启动时就会将 C 类加载起来,那

    么在 B 类进⾏加载时就不需要在重复加载 C 类了。

  • 安全: 防止核心 API 库被随意篡改。比如用户自定义了一个 java.lang.Object 类,如果没有双亲委派,它会被加载,从而破坏 Java 体系。但有了它,这个请求会最终委派给启动类加载器,而启动类加载器加载的是核心的 Object 类,用户的这个类就无法被加载。


3. 打破双亲委派模型
在某些场景下,需要打破双亲委派模型,实现自己的类加载逻辑,例如:

  • 从非标准来源加载类(如网络、数据库)。

  • 实现热部署、热替换(如 Tomcat 为每个 Web 应用提供独立的类加载器)。

  • 对类进行加密,需要在加载时解密。

如何实现:

通常继承 java.lang.ClassLoader 类,然后重写 findClass(String name) 方法。在这个方法中,编写如何获取类的字节码并调用 defineClass 方法来定义类的逻辑。

java 复制代码
public class MyClassLoader extends ClassLoader {
    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 1. 根据 name 和 classPath,找到 .class 文件,读取为字节数组 byte[]
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            // 2. 调用 defineClass 方法,将字节数组转换为 Class 对象
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        // 实现从特定路径(如文件、网络)读取 .class 文件的逻辑
        // 返回字节数组
        // ... (具体实现省略)
        return null;
    }
}

总结

概念 描述
过程 加载 (获取字节流 -> 方法区 -> 生成Class对象) -> 链接(验证 -> 准备 -> 解析) -> 初始化(执行 )
类加载器 Bootstrap(核心库) -> Extension(扩展库) -> Application(用户ClassPath)
双亲委派 子加载器将加载请求委派给父加载器,父加载器无法完成时子加载器才自己加载。保证了安全性和唯一性。
自定义 继承 ClassLoader,重写 findClass 方法,用于实现非标准来源的类加载、热部署等高级功能。

理解类加载机制是深入理解 JVM 工作原理、实现高级特性和解决复杂类冲突问题的关键。

相关推荐
MZ_ZXD0012 小时前
springboot旅游信息管理系统-计算机毕业设计源码21675
java·c++·vue.js·spring boot·python·django·php
PP东2 小时前
Flowable学习(二)——Flowable概念学习
java·后端·学习·flowable
ManThink Technology2 小时前
如何使用EBHelper 简化EdgeBus的代码编写?
java·前端·网络
invicinble2 小时前
springboot的核心实现机制原理
java·spring boot·后端
人道领域2 小时前
SSM框架从入门到入土(AOP面向切面编程)
java·开发语言
大模型玩家七七3 小时前
梯度累积真的省显存吗?它换走的是什么成本
java·javascript·数据库·人工智能·深度学习
CodeToGym3 小时前
【Java 办公自动化】Apache POI 入门:手把手教你实现 Excel 导入与导出
java·apache·excel
凡人叶枫3 小时前
C++中智能指针详解(Linux实战版)| 彻底解决内存泄漏,新手也能吃透
java·linux·c语言·开发语言·c++·嵌入式开发
JMchen1234 小时前
Android后台服务与网络保活:WorkManager的实战应用
android·java·网络·kotlin·php·android-studio
阔皮大师4 小时前
INote轻量文本编辑器
java·javascript·python·c#