JVM 类加载器如何保证核心类库不被覆盖?

#Java#面试八股

JVM 类加载器如何保证核心类库不被覆盖?

结论先行:靠四道闸门层层拦截:双亲委派让加载请求先交给启动类加载器(bootstrap loader),核心类在它那里就命中返回,用户类根本没有机会定义同名类;包名保护在 ClassLoader.preDefineClass 里硬性拒绝任何加载器定义 java.* 开头的类;启动类加载器不可见,它由 C++ 实现、在 Java 层拿不到引用,无法被替换;模块封装(JDK 9+)进一步限制对 JDK 内部包的访问。四道闸门任意一道生效,就足以阻止「伪造 java.lang.String」。

一、四道闸门

闸门生效位置拦截效果
双亲委派ClassLoader.loadClass请求先给父加载器,核心类在 bootstrap 命中,子加载器不再尝试
包名检查ClassLoader.preDefineClass非 bootstrap 加载器定义 java.* 直接抛 SecurityException
启动加载器不可见getParent() 返回 nullJava 代码无法得到它的引用,无法替换或包装
模块封装JPMS(JDK 9+)未导出包不可访问,非法的跨模块反射需 --add-opens
用户代码: new MyLoader().loadClass("java.lang.String")
      │
      ▼
MyLoader.loadClass ──委派──→ AppClassLoader ──委派──→ Platform ──委派──→ Bootstrap
                                                                          │
                                                   命中:返回 JDK 的 String
      │
      └─ 若 Bootstrap 找不到(理论上不会),MyLoader 才 findClass
            └─ defineClass("java.lang.String") → preDefineClass 抛
               SecurityException: Prohibited package name: java.lang

二、亲手验证一次

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class HijackDemo extends ClassLoader {
    private final Path classFile;

    public HijackDemo(Path classFile) {
        super(null);                     // parent=null 也无法绕过包名检查
        this.classFile = classFile;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] bytes = Files.readAllBytes(classFile);   // 自己伪造的 java.lang.String.class
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(name, e);
        }
    }

    public static void main(String[] args) throws Exception {
        HijackDemo loader = new HijackDemo(Path.of("fake/java/lang/String.class"));
        Class<?> c = loader.loadClass("java.lang.String");    // 委派给 bootstrap,直接命中真类
        System.out.println(c.getClassLoader());                // null(bootstrap 加载)

        loader.findClass("java.lang.String");                  // 绕开委派直接定义
    }
}
// 输出:
// null
// Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang

注意两点:loadClass 走委派后拿到的是真类,getClassLoader() 为 null 正是 bootstrap 的标志;而 findClass 直接调用时,defineClass 的包名检查会立刻拒绝。

三、历史上的「合法」覆盖

手段版本现状
-Xbootclasspath/p: 前置替换 rt.jar 内容JDK 8 及以前JDK 9 起移除(-Xbootclasspath 系列全部删除)
把 jar 放进 jre/lib/extJDK 8 及以前JDK 9 起扩展机制被模块系统取代
--patch-module java.base=xxx.jarJDK 9+仍可用,但需显式打补丁,属测试/诊断手段,生产不建议
自定义加载器加载 java.*所有版本始终被拒绝

所以「核心类库不可被覆盖」并不是绝对的密码学保证,而是「默认路径全部封死,只剩需要显式授权的诊断入口」。JDK 自己升级、打补丁用的就是 --patch-module 这类机制。

四、常见追问

追问回答要点
双亲委派为什么能保证唯一性同一个类名最终都由同一个父加载器定义,方法区里只有一份元数据
自己写加载器能加载 java.lang.String 吗不能,preDefineClass 检查包名,抛 SecurityException
加密 class 的加载器怎么工作覆写 findClass,先解密再 defineClass,委派语义仍然保留
类隔离(多版本共存)怎么实现每个版本一个加载器,且被隔离的类不能是 java.* 这类核心类
模块化之后还有什么保护默认强封装,未导出包连反射都要 --add-opens,比包名检查更细

小结

核心类库安全靠「委派优先 + 包名白名单 + 引导加载器不可触达 + 模块封装」四层。答题时能指出 preDefineClass 和 Prohibited package name 这条硬拦截,比只说「双亲委派」更有说服力。

笔记加载中…