类加载:双亲委派是妈宝行为吗?(手撕自定义类加载器)

105 阅读3分钟

1.3 类加载:双亲委派是妈宝行为吗?(手撕自定义类加载器)


▍ 类加载器的"家族企业"

想象一下,Java 的类加载器就像一个家族企业:

  • Bootstrap 加载器:家族创始人,只负责核心业务(加载 java.lang 等包)
  • Extension 加载器:家族二代,分管扩展业务(jre/lib/ext 目录)
  • Application 加载器:家族三代,负责日常业务(你的 classpath 代码)

双亲委派 就是家族企业的规矩:
"儿子,这个活你能干吗?不能?那让爸爸来!"
(子加载器先让父加载器尝试加载类)


▍ 为什么需要"妈宝行为"?

用一个现实场景解释双亲委派的意义:

// 假如你自己写了个 java.lang.String
public class String {
    public static void main(String[] args) {
        System.out.println("我是黑客!");
    }
}

如果没有双亲委派:
JVM 会加载你的 String 类,导致系统崩溃
有了双亲委派:
Application 加载器会先让爸爸(Bootstrap)加载官方 String
你的山寨 String 根本没机会执行!


▍ 手撕自定义类加载器

Step 1:创建测试类

// 文件路径:src/main/java/com/example/MagicClass.java
package com.example;

public class MagicClass {
    static {
        System.out.println("魔法类被加载!加载器是:" 
            + MagicClass.class.getClassLoader());
    }
}

Step 2:编译并生成 class 文件

javac MagicClass.java
# 将生成的 MagicClass.class 复制到项目根目录的 classes/com/example 下

Step 3:自定义类加载器

import java.nio.file.Files;
import java.nio.file.Paths;

public class CustomLoader extends ClassLoader {
    // 指定类文件存储路径(比如:classes/com/example/MagicClass.class)
    private final String classPath = "classes/";

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // 1. 读取字节码文件
            String path = name.replace('.', '/') + ".class";
            byte[] bytes = Files.readAllBytes(Paths.get(classPath + path));
            
            // 2. 定义类(关键方法)
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            throw new ClassNotFoundException("类没找到: " + name);
        }
    }

    public static void main(String[] args) throws Exception {
        CustomLoader loader = new CustomLoader();
        
        // 用自定义加载器加载类
        Class<?> magicClass = loader.loadClass("com.example.MagicClass");
        
        // 创建实例(触发静态代码块)
        Object instance = magicClass.getDeclaredConstructor().newInstance();
        System.out.println("实例创建成功:" + instance);
    }
}

运行结果:

魔法类被加载!加载器是:CustomLoader@1b6d3586
实例创建成功:com.example.MagicClass@4554617c

▍ 双亲委派流程图解

  ┌───────────────────┐
  │   自定义加载器      │
  │  (CustomLoader)   │
  └─────────┬─────────┘
            │ 1. 尝试加载类
            ▼
  ┌───────────────────┐
  │ ApplicationClassLoader │
  └─────────┬─────────┘
            │ 2. 向上委派
            ▼
  ┌───────────────────┐
  │ ExtensionClassLoader │
  └─────────┬─────────┘
            │ 3. 继续向上
            ▼
  ┌───────────────────┐
  │ BootstrapClassLoader │
  └─────────┬─────────┘
            │ 4. 所有父加载器
            │    都加载失败
            ▼
  ┌───────────────────┐
  │ 自定义加载器开始加载 │
  │   (findClass())   │
  └───────────────────┘

▍ 技术要点总结

  1. 打破双亲委派
    如果直接重写 loadClass 方法而不调用 super.loadClass(),就能打破双亲委派
    (但 99% 的情况下不要这么做!)

  2. 沙箱安全机制
    自定义加载器加载的类无法直接访问核心库的包(如 java.lang

  3. 热部署原理
    通过创建新的类加载器加载修改后的类,旧类会被 GC 回收


▍ 面试灵魂拷问

面试官:双亲委派有什么缺点?
:就像家族企业容易僵化,某些场景需要"叛逆":

  • Tomcat 需要隔离不同 Web 应用的类
  • SPI 机制需要反向委派(如 JDBC 驱动加载)
    这时候就要打破双亲委派!

(此时可以掏出手机展示刚写的自定义加载器代码)
"这是我实现的热部署 Demo,您要看看吗?"


▍ 课后彩蛋

尝试修改代码:

  1. findClass 方法中添加 System.out.println("正在加载: " + name)
  2. 观察加载 java.lang.String 时会发生什么
  3. 思考为什么输出结果中没有打印核心类的加载信息

答案:Bootstrap 加载器是用 C++ 实现的,Java 层看不到它的加载过程!