从零起步学习JVM || 第一章:类加载器与双亲委派机制模型详解

前言

这是最基础、最常用的场景 ------ 你写的每一行 Java 代码,运行时都会隐式触发类加载器工作,只是 JVM 帮你做了所有操作,你感知不到。

1. 程序启动时(最核心的默认场景)

当你执行 java Test 启动程序时,JVM 会立即触发类加载器:

  • 首先加载Test类(应用类加载器);
  • Test类中引用的其他类(比如UserArrayList),会在首次使用时被加载;
  • 核心类(如StringObject)由启动类加载器加载,无需你干预。

例子

java 复制代码
public class Test {
    public static void main(String[] args) {
        // 首次使用User类,触发应用类加载器加载User.class
        User user = new User(); 
        // 首次使用ArrayList,触发启动类加载器加载java.util.ArrayList
        List<String> list = new ArrayList<>(); 
    }
}

本质:只要你使用一个类(创建实例、调用静态方法 / 字段、继承 / 实现),JVM 就会自动调用类加载器加载这个类

2. 触发类加载的具体行为(JVM 规范定义)

JVM 规范明确了 6 种必须触发类加载的场景(称为 "主动使用"),只要满足其一,类加载器就会工作:

主动使用场景 例子 触发的类加载器
创建类的实例 new User() 应用类加载器
调用类的静态方法 User.staticMethod() 应用类加载器
访问类的静态字段(非 final) System.out.println(User.staticField) 应用类加载器
反射调用类 Class.forName("User") 应用类加载器
初始化子类 子类Student extends User,加载 Student 时先加载 User 应用类加载器
启动包含 main 方法的主类 java Test 应用类加载器

补充:final静态常量(如public static final int NUM = 100)是编译期常量,直接存入运行时常量池,不会触发类加载。

简单来说:你写的 Java 代码能运行,第一步就是类加载器把对应的.class 文件加载到内存;而当你需要灵活控制类的加载规则时,就需要主动使用类加载器

一、明确类加载器的核心工作

类加载器的核心工作就是把硬盘上的.class文件(字节码)加载到 JVM 内存中,并生成对应的Class对象,让程序能使用这个类。我会用通俗的场景 + 代码例子,帮你彻底看懂类加载器到底在做什么。

类加载器就像 "JVM 的文件搬运工":

  1. 从指定位置(硬盘、网络、内存等)找到编译好的.class文件;
  2. .class文件的字节码读取到 JVM 内存中;
  3. 将字节码转换为 JVM 能识别的java.lang.Class对象(这个对象是类的 "元数据模板",程序通过它创建实例);
  4. 同时负责类的唯一性校验(同一个类被不同类加载器加载,会被 JVM 视为不同的类)。

二、用生活场景举例:类比理解类加载器

假设你是一家公司的员工(JVM),需要用一份 "员工手册"(Java 类,比如User.class):

  • .class文件 = 打印好的纸质手册(存在公司文件柜里);
  • 类加载器 = 行政助理(专门负责找手册、复印、送到你手上);
  • Class对象 = 你手上的手册复印件(你能直接看、用,对应程序能通过Class对象创建实例);
  • 核心动作:行政助理(类加载器)从文件柜(硬盘)找到手册(.class)→ 复印(加载到内存)→ 给你(生成Class对象)→ 你用手册做事(程序用Class对象创建User实例)。

三、代码例子:直观看到类加载器的工作过程

我们通过代码,一步步看类加载器如何加载类、生成Class对象:

步骤 1:编写一个简单的 Java 类(生成.class文件)
java 复制代码
// User.java
public class User {
    private String name;
    
    public User(String name) {
        this.name = name;
    }
    
    public void sayHello() {
        System.out.println("你好,我是" + name);
    }
}

执行javac User.java,生成User.class文件(存放在硬盘上,比如/Users/xxx/目录下)。

步骤 2:用代码展示类加载器的加载过程
java 复制代码
// ClassLoaderDemo.java
public class ClassLoaderDemo {
    public static void main(String[] args) throws Exception {
        // 1. 获取系统类加载器(默认加载我们自己写的类)
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("默认类加载器:" + systemClassLoader.getClass().getName());
        
        // 2. 类加载器的核心工作:加载User.class,生成Class对象
        //    这里的"User"是类的全限定名(如果有包,要写包名+类名,比如com.example.User)
        Class<?> userClass = systemClassLoader.loadClass("User");
        
        // 3. 验证:加载后生成了Class对象(这就是类加载器的核心产物)
        System.out.println("加载后生成的Class对象:" + userClass);
        System.out.println("该Class对象的类加载器:" + userClass.getClassLoader().getClass().getName());
        
        // 4. 通过Class对象创建实例(类加载的最终目的)
        User user = (User) userClass.getConstructor(String.class).newInstance("Java学习者");
        user.sayHello(); // 调用方法,验证类加载成功
    }
}
步骤 3:运行结果(关键解读)
复制代码
默认类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader
加载后生成的Class对象:class User
该Class对象的类加载器:jdk.internal.loader.ClassLoaders$AppClassLoader
你好,我是Java学习者
代码关键解读(类加载器的工作细节)
  1. ClassLoader.getSystemClassLoader() :获取 JVM 默认的 "应用类加载器"(负责加载我们自己写的类,比如User);
  2. loadClass("User") :类加载器的核心方法,做了 3 件事:
    • 查找:在指定路径(当前目录)找到User.class文件;
    • 读取:把User.class的字节码读取到 JVM 内存;
    • 转换:将字节码转换为Class<User>对象,存入方法区(元空间);
  3. 通过Class对象创建实例 :类加载器加载类的最终目的,是让程序能通过Class对象创建实例、调用方法 ------ 如果没有类加载器,JVM 根本不知道User类的存在,更无法使用它。

四、扩展:不同类加载器的分工(理解类加载器的设计)

JVM 内置了 3 类核心类加载器,分工明确(就像公司不同级别的行政):

类加载器类型 核心工作(加载范围) 类比场景
启动类加载器(Bootstrap) 加载 JDK 核心类(如java.lang.Stringjava.util.ArrayList),存放在rt.jar 公司总部的行政,负责核心制度文件
扩展类加载器(Extension) 加载 JDK 扩展类(如javax.开头的类),存放在jre/lib/ext目录 分公司行政,负责扩展制度文件
应用类加载器(Application) 加载我们自己写的类、第三方 jar 包(如 Spring、MyBatis) 部门行政,负责部门专属文件
(注:启动类加载器是 JVM 底层 C++ 实现,不是 Java 类,所以getClassLoader()返回null

总结

  1. 类加载器的核心工作:找到.class文件 → 读取字节码到内存 → 生成Class对象,让 JVM 能识别并使用这个类;
  2. 类加载器不是单一的,而是分工明确的 "加载器体系"(启动 / 扩展 / 应用类加载器);
  3. 最终目的:生成Class对象,程序通过这个对象创建实例、调用方法 ------ 没有类加载器,所有 Java 类都只是硬盘上的.class文件,无法被 JVM 执行。

你的核心理解可以简化为:类加载器是 "连接硬盘.class文件和 JVM 内存的桥梁",没有它,Java 代码编译后的字节码永远无法被运行。

五、双亲委派的核心定义

双亲委派模型 是 JVM 类加载器加载类时遵循的 "向上委托、向下查找" 规则:当一个类加载器收到加载类的请求时,它不会自己先加载,而是把请求委托给它的 "父类加载器" 去完成;只有当父类加载器无法加载(找不到对应的.class 文件)时,子加载器才会自己尝试加载。

注意:这里的 "父类加载器" 不是 Java 继承关系的父类,而是逻辑上的父子关系(比如应用类加载器的父加载器是扩展类加载器,扩展类加载器的父加载器是启动类加载器)。

用生活场景类比:理解双亲委派的逻辑

假设你(应用类加载器)在公司要找一份文件(加载 User 类):

  1. 你先把找文件的请求交给你的直属领导(扩展类加载器);
  2. 领导又把请求交给公司老板(启动类加载器);
  3. 老板先找:如果老板有这份文件(比如是 JDK 核心类),直接给你,流程结束;
  4. 如果老板没有,领导再找:领导有就给你,没有则流程到你;
  5. 最后你自己找:你找到自己的文件(User 类),交给使用方。

这个 "先找上级、上级找不到自己再找" 的逻辑,就是双亲委派的核心。

六、理解双亲委派的执行过程

我们以加载User类(自己写的类)和String类(JDK 核心类)为例,拆解双亲委派的执行步骤:

1. 先明确类加载器的父子关系(从上到下)
复制代码
启动类加载器(Bootstrap)← 扩展类加载器(Extension)← 应用类加载器(Application)

(箭头表示 "父级",应用类加载器是最底层的子加载器)

2. 加载 JDK 核心类(String)的流程(双亲委派的 "上级处理")

当程序需要加载java.lang.String时:

  • 结果:String 类由启动类加载器加载,应用类加载器全程只是 "转发请求"。
3. 加载自定义类(User)的流程(双亲委派的 "自己处理")

当程序需要加载User类时:

  • 结果:只有父加载器都加载失败时,应用类加载器才自己加载 User 类。
注:JDK 9 + 把扩展类加载器改名为平台类加载器(PlatformClassLoader),逻辑不变。

七、双亲委派的核心作用(为什么要设计这个规则?)

  1. 保证核心类的唯一性 :避免自定义类覆盖 JDK 核心类(比如你自己写一个java.lang.String类,双亲委派会让启动类加载器先加载 JDK 的 String,你的自定义 String 永远不会被加载,防止核心类被篡改);
  2. 保证类加载的安全性 :核心类只能由启动类加载器加载,避免恶意代码替换核心类(比如替换java.lang.Object);
  3. 优化类加载效率:核心类只需要启动类加载器加载一次,所有子加载器都能复用,无需重复加载。

八、反例:打破双亲委派(了解即可)

有些场景需要打破双亲委派(比如 Tomcat 的类加载器):Tomcat 需要为不同 Web 应用加载各自的类,即使类名相同,也要视为不同类,因此 Tomcat 的类加载器会先自己加载,加载不到再委托父加载器(反向委派)。但这是特殊场景,JVM 默认遵循双亲委派。

九、打破双亲委派核心方法

方法 1:重写 ClassLoader 的 loadClass () 方法(最核心、最常用)

JVM 默认的ClassLoader.loadClass()方法是双亲委派的核心实现,源码逻辑简化如下:

java 复制代码
// 父类ClassLoader的默认loadClass逻辑(双亲委派)
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        // 1. 先检查该类是否已加载
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 2. 委托父加载器加载(核心:先找父类)
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 父加载器是启动类加载器,直接找核心类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 父加载器加载失败,抛出异常
            }

            if (c == null) {
                // 3. 父加载器失败,自己加载(findClass是子类实现)
                long t1 = System.nanoTime();
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

打破方式 :重写loadClass()方法,调换 "委托父加载器" 和 "自己加载" 的顺序 ------ 先自己加载,加载失败再委托父加载器。

java 复制代码
// 自定义类加载器:打破双亲委派
public class BreakParentDelegationClassLoader extends ClassLoader {
    private String rootPath; // 自定义类加载路径

    public BreakParentDelegationClassLoader(String rootPath, ClassLoader parent) {
        super(parent); // 指定父加载器
        this.rootPath = rootPath;
    }

    // 重写loadClass方法,打破双亲委派
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 1. 检查类是否已加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                // 2. 先自己加载(打破核心:优先自己,而非父加载器)
                try {
                    c = findClass(name); // 自己加载自定义路径下的类
                } catch (ClassNotFoundException e) {
                    // 3. 自己加载失败,再委托父加载器
                    if (getParent() != null) {
                        c = getParent().loadClass(name);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

    // 实现findClass:加载自定义路径下的.class文件
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 类名转文件路径:com.example.User → rootPath/com/example/User.class
            String filePath = rootPath + name.replace(".", "/") + ".class";
            byte[] classBytes = Files.readAllBytes(Paths.get(filePath));
            // 把字节码转为Class对象
            return defineClass(name, classBytes, 0, classBytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException("类加载失败:" + name, e);
        }
    }

    // 测试:加载自定义路径下的类,优先使用自己的加载器
    public static void main(String[] args) throws Exception {
        // 自定义类加载路径(比如D:/custom_classes/)
        String customPath = "D:/custom_classes/";
        // 创建自定义加载器,父加载器为应用类加载器
        BreakParentDelegationClassLoader customLoader = new BreakParentDelegationClassLoader(customPath, ClassLoader.getSystemClassLoader());
        
        // 加载自定义路径下的com.example.User类
        Class<?> userClass = customLoader.loadClass("com.example.User");
        System.out.println("类加载器:" + userClass.getClassLoader().getClass().getName());
        // 验证:即使父加载器能找到该类,也会优先用自定义加载器加载
    }
}

核心说明

  • 重写loadClass()后,优先执行findClass()(自己加载),失败后才委托父加载器;
  • 这是 Tomcat 类加载器的核心实现逻辑(Tomcat 为每个 Web 应用创建独立加载器,优先加载应用内的类,避免和其他应用 / 核心类冲突)。

总结

  1. 双亲委派的核心规则:先委托父加载器加载,父加载器失败后子加载器才自己加载
  2. 核心目的:保证 JDK 核心类的唯一性和安全性,避免核心类被篡改或重复加载;
  3. 执行流程:应用类加载器 → 扩展类加载器 → 启动类加载器(向上委托),加载失败则反向向下查找。

记住这个核心逻辑:双亲委派是 "先找上级,上级不行自己来",本质是为了保护 JVM 核心类的安全和唯一性。

相关推荐
黎雁·泠崖5 小时前
【魔法森林冒险】2/14 抽象层设计:Figure/Person类(所有角色的基石)
java·开发语言
Java编程爱好者5 小时前
Seata实现分布式事务:大白话全剖析(核心讲透AT模式)
后端
神奇小汤圆5 小时前
比MySQL快800倍的数据库:ClickHouse的性能秘密
后端
IvorySQL5 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
小小张说故事5 小时前
BeautifulSoup:Python网页解析的优雅利器
后端·爬虫·python
怒放吧德德5 小时前
后端 Mock 实战:Spring Boot 3 实现入站 & 出站接口模拟
java·后端·设计
·云扬·5 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
浅念-5 小时前
C语言编译与链接全流程:从源码到可执行程序的幕后之旅
c语言·开发语言·数据结构·经验分享·笔记·学习·算法
IT邦德5 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle