【JVM】Java类加载机制

【JVM】Java类加载机制

什么是类加载?

在 Java 的世界里,每一个类或接口在经过编译后,都会生成对应的 .class 字节码文件。

所谓类加载机制 ,就是 JVM 将这些 .class 文件中的二进制数据加载到内存中 ,并对其进行校验、解析、初始化 等一系列操作。最终,每个类都会在方法区(或元空间)中保留一份结构化的类信息(元数据),并在Java 堆中创建一个 java.lang.Class 类型的对象,供程序运行时使用。

从 JVM 的角度看,一个类的生命周期包括以下 7 个阶段:

加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载

其中,前五个阶段(加载、验证、准备、解析、初始化)统称为类的加载过程 。其中,验证、准备和解析这三个阶段可以统称为连接

需要注意的是,类加载的五个阶段并不是严格按顺序线性执行的,而是相互交叉、动态混合的过程

例如:

  • 部分验证操作(如文件格式验证)可能在加载 .class 文件的过程中就被触发。
  • 符号引用的解析可能被延迟到真正使用时才发生。

类加载的详细过程

1.加载

  • 任务: 查找并加载类的二进制数据(通常是 .class 文件,但来源可以多样)。

  • 过程:

    • 通过类的全限定名 (如 java.lang.Stringcom.example.MyClass)获取定义此类的二进制字节流。这个字节流来源可以是文件系统(最常见的)、网络、ZIP/JAR包、运行时计算生成(动态代理)、数据库等等。

    类的全限定名就是类的完整名称,即:包名 + 类名(如 java.lang.Stringcom.example.MyClass

    • 将这个字节流所代表的静态存储结构转换为方法区 的运行时数据结构。
    • 堆(Heap) 内存中创建一个代表这个类的 java.lang.Class 对象,作为方法区中这个类的各种数据的访问入口。常用的 .classgetClass() 返回的就是这个对象。
    java 复制代码
    public class CustomLoader extends ClassLoader {
        @Override
        protected Class<?> findClass(String name) {
            // 1. 获取字节流(可来自文件/网络/数据库等)
            byte[] bytes = loadClassBytes(name); 
            
            // 2. 转换为方法区数据结构
            // 3. 创建Class对象
            return defineClass(name, bytes, 0, bytes.length);
        }
    }
  • 关键点:

    • 类加载器 完成。

    • 加载的最终产物是堆中的 Class 对象。

    • 类的加载是懒惰的,首次用到时才会加载:

      1. 使用了类.class
      2. 用类加载器的 loadClass 方法加载类
      3. 满足类的初始化条件(后文有详细介绍)

      参考:2-类加载_验证类加载是懒惰的_哔哩哔哩_bilibili

2.连接

2.1 验证
  • 任务: 确保加载的字节码信息符合 JVM 规范,是安全、无害的,不会危害虚拟机自身安全。
  • 内容:
    • 文件格式验证(魔数、版本号等)
    • 元数据验证(语义分析,如是否有父类、是否继承了final类、是否实现接口所有方法等)
    • 字节码验证(数据流和控制流分析,确保逻辑正确,如操作数栈类型匹配、跳转指令目标合理等)
    • 符号引用验证(检查常量池中的符号引用能否找到对应的类、字段、方法等)。
  • 重要性: 保护JVM安全,防止恶意代码或损坏的字节码文件导致JVM崩溃或执行危险操作。虽然耗点时间,但对系统稳定性至关重要。
2.2 准备
  • 任务: 为类的静态变量 分配内存,并设置默认初始值
  • 过程:
    • 在方法区中为这些静态变量分配内存空间。
    • 设置默认初始值:
      • 基本类型:int -> 0, long -> 0L, float -> 0.0f, double -> 0.0d, char -> '\u0000', boolean -> false
      • 引用类型:null
  • 关键点:
    • 这里分配内存并初始化的是类变量(static变量),不是实例变量
    • 初始化的值是默认零值,不是代码中显式赋的值 (如 public static int value = 123;,在准备阶段后 value0,赋值 123 的操作发生在后面的初始化阶段)。
    • 如果静态变量是 final 修饰的基本类型或 String 常量 ,并且在编译时就能确定值(如 public static final int CONSTANT = 100;),那么这个值会直接在准备阶段被赋予(此时 CONSTANT 就是 100)。
2.3 解析
  • 任务: 将常量池内的符号引用 替换为直接引用
  • 符号引用与直接引用:
    • 符号引用: 一组描述被引用目标(类、字段、方法)的符号。例如,java/lang/Object(类名)、toString:()Ljava/lang/String;(方法名和描述符)。它只是一个字面量引用,与内存布局无关。
    • 直接引用: 一个能直接定位到目标(类在方法区的地址、字段或方法在内存中的偏移量或句柄)的指针、偏移量或句柄。它是与JVM运行时内存布局相关的。
  • 过程: JVM 查找符号引用所指向的类、字段或方法的实际位置,并将常量池中的符号引用替换为指向该位置的直接引用。
  • 时机: 解析阶段可以在初始化之前完成,也可以在初始化之后完成(甚至延迟到第一次实际使用该符号引用时),这取决于 JVM 的实现策略("及早解析"或"惰性解析")。

3.初始化

  • 任务: 执行类的初始化代码 ,主要是执行类构造器 <clinit>() 方法。
  • <clinit>() 方法:
    • 由编译器自动收集类中所有类变量(static变量)的显式赋值动作静态代码块(static {} 块) 中的语句合并生成。
    • 顺序:按源代码中出现的顺序执行。
    • 父类的 <clinit>() 优先于子类的执行。
    • 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确地加锁、同步(即线程安全)。如果一个线程正在执行它,其他线程会阻塞等待。
  • 触发时机(严格规定): 类只有在以下 6 种情况之一发生时,才会立即进行初始化(加载和连接可能更早发生):
    1. 创建类的实例 (new)。
    2. 访问类的静态变量(读取或赋值) ,除非该静态变量是 final 常量并且在编译期就能确定值(常量传播优化)。
    3. 调用类的静态方法 (static 方法)。
    4. 使用反射 (Class.forName("..."), getMethod 等) 对类进行反射调用。
    5. 初始化一个类的子类时,会触发其父类的初始化。
    6. JVM 启动时被标明为启动类(包含 main() 方法的那个类)。
  • 关键点:
    • 这是类加载过程的最后一步
    • 此时才真正执行程序员的代码逻辑(静态赋值、静态块)。
    • 之前的"准备"阶段只是分配内存并赋零值,这里是赋程序员定义的值。

类加载器

在类加载的第一个阶段------加载中,JVM 需要根据类的全限定名,找到并读取其对应的字节码文件(.class 文件)。

这个查找和读取 .class 字节流的工作,正是由类加载器来完成的。

JVM 中内置了三个重要的 ClassLoader

  1. Bootstrap ClassLoader (启动类/引导类加载器):
    • 用原生代码(C/C++)实现,是 JVM 自身的一部分。
    • 负责加载 JAVA_HOME/lib 目录下的核心 Java 库(如 rt.jar, charsets.jar)或 -Xbootclasspath 参数指定的路径中的类。
    • 是最高级别的加载器,没有父加载器
    • null 表示: 在 Java 代码中试图获取它的引用时,返回 null
  2. Extension ClassLoader (扩展类加载器):
    • sun.misc.Launcher$ExtClassLoader 实现(Java)。
    • 负责加载 JAVA_HOME/lib/ext 目录下的扩展库,或 java.ext.dirs 系统变量指定的路径中的所有类库。
    • 父加载器是 Bootstrap ClassLoader
  3. Application ClassLoader (应用程序类加载器 / 系统类加载器):
    • sun.misc.Launcher$AppClassLoader 实现(Java)。
    • 负责加载用户类路径(ClassPath) 上所指定的类库。这是我们程序中默认的类加载器。
    • 父加载器是 Extension ClassLoader
    • 通过 ClassLoader.getSystemClassLoader() 可以获取到它。

除了这三种类加载器之外,用户还可以加入自定义的类加载器来进行拓展,以满足自己的特殊需求:

除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。如果我们要自定义自己的类加载器,需要继承 ClassLoader抽象类。

ClassLoader 类中有两个核心方法:

  • protected Class<?> loadClass(String name, boolean resolve)

    加载指定名称的类。该方法实现了双亲委派模型 :会先委托给父加载器尝试加载,如果父加载器无法完成,才会调用自身的 findClass() 方法进行加载。

    java 复制代码
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 先检查当前类是否已经加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        // 先让父类加载器尝试加载
                        c = parent.loadClass(name, false);
                    } else {
                        // 如果没有父加载器(即引导类加载器),使用 bootstrap 加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 忽略异常,进入下一步由自己加载
                }
    
                if (c == null) {
                    // 如果父加载器无法加载,再尝试使用当前类加载器加载
                    c = findClass(name);
                }
            }
    
            if (resolve) {
                resolveClass(c);
            }
    
            return c;
        }
    }
  • protected Class<?> findClass(String name)

    根据类名查找类的定义并返回对应的 Class 对象。默认实现是抛出 ClassNotFoundException,需要我们在子类中重写。

    java 复制代码
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

双亲委派模型简介

双亲委派模型是 Java 类加载机制的重要组成部分,它通过委派父加载器优先加载类的方式,实现了两个关键的安全目标:避免类的重复加载和防止核心 API 被篡改。

  • 工作流程: 当一个类加载器收到加载类的请求时:

    1. 它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
    2. 每一层的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器
    3. 只有当父加载器反馈自己无法完成这个加载请求 (在它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载
  • 核心思想: "向上委派,向下加载"。

  • 核心目的:

    • 保证基础类的唯一性和安全性: 防止用户自定义一个与核心库(如 java.lang.Object)同名的类被加载,从而覆盖核心库的行为(沙箱安全机制)。
    • 避免重复加载: 父加载器已经加载过的类,子加载器就不会再加载(在同一个命名空间内)。

内容参考

【JVM】Java类加载机制这块算是玩明白了_哔哩哔哩_bilibili

类加载器详解(重点)

相关推荐
抓饼先生1 天前
Linux control group笔记
linux·笔记·bash
ue星空1 天前
月2期学习笔记
学习·游戏·ue5
我没想到原来他们都是一堆坏人1 天前
(未完待续...)如何编写一个用于构建python web项目镜像的dockerfile文件
java·前端·python
搞一搞汽车电子1 天前
S32K3平台eMIOS 应用说明
开发语言·驱动开发·笔记·单片机·嵌入式硬件·汽车
萧邀人1 天前
第二课、熟悉Cocos Creator 编辑器界面
学习
沙二原住民1 天前
提升数据库性能的秘密武器:深入解析慢查询、连接池与Druid监控
java·数据库·oracle
三毛20041 天前
玳瑁的嵌入式日记D33-0908(SQL数据库)
jvm·数据库·sql
Mr_Xuhhh1 天前
sqlite3的使用
jvm·oracle·sqlite
Jerry&Grj1 天前
SpringBoot埋点功能技术实现方案深度解析:架构设计、性能优化与扩展性实践
java·微服务·性能优化·springboot·架构设计·埋点技术
没有bug.的程序员1 天前
Redis Stream:轻量级消息队列深度解析
java·数据库·chrome·redis·消息队列