学类加载器的时候,我被“谁加载谁“绕了好几天

学类加载器的时候,我被“谁加载谁“绕了好几天 刚开始学 JVM 类加载机制的时候我最头疼的不是加载过程分几步而是这几层类加载器到底谁是谁的爸爸。启动类加载器、扩展类加载器、应用类加载器、自定义类加载器……名字一堆关系图一画就乱。而且学完之后我一度觉得这东西离业务代码很远平时写 Spring Boot 又用不到。后来踩了几个坑才明白类加载器的设计直接关系到 Java 的安全性和动态性不搞懂不行。类加载器到底是什么为什么需要分层类加载器的任务很简单把一个类的二进制字节码读进 JVM转换成java.lang.Class对象。但问题在于Java 程序运行的时候会从很多不同的地方加载类。比如java.lang.String是从 JDK 核心库来的你项目里引的第三方 jar 是从 Maven 仓库来的你自己写的类在 target/classes 里。JVM 需要区分这些类的来源不能让一个来自不可信来源的类冒充核心类。这就是分层设计的出发点。你可以把类加载器想象成一层一层的安检。最内层由 JVM 自己把守只放行最核心的类。越往外信任度越低但灵活性越高。四层类加载器每层的职责不一样1. 启动类加载器Bootstrap Class LoaderJVM 自己管的这是所有类加载器里地位最高的一个。它负责加载 Java 最核心的类库。它加载什么在 Java 8 及以前它加载的是jre/lib/rt.jar。rt.jar里装的是java.lang.*、java.util.*、java.io.*这些你每天都在用的类。从 Java 9 开始rt.jar被移除了核心类库以模块化的形式存放在$JAVA_HOME/lib/modules里比如java.base模块。它特殊在哪启动类加载器不是用 Java 写的而是 C 实现的是 JVM 的一部分。所以在 Java 代码里你拿不到它的对象调用getClassLoader()返回的是null。我第一次看到这个设计的时候有点懵返回 null 不会空指针吗后来才知道这算是 JVM 约定好的信号看到 null 就知道这个类是 Bootstrap ClassLoader 加载的不是 Bug。2. 平台类加载器Platform / Extension Class Loader中间层这一层的名字在 Java 8 和 Java 9 不一样。Java 8 及以前扩展类加载器Extension Class Loader它负责加载jre/lib/ext目录下的扩展 jar 包。你也可以通过java.ext.dirs系统属性指定额外的扩展目录。这个设计本意是让 JDK 可以在不修改核心库的前提下扩展功能。Java 9 及以后平台类加载器Platform Class LoaderJava 9 模块化之后jre/lib/ext目录和java.ext.dirs属性都被移除了。新的平台类加载器负责加载 JDK 中除核心模块之外的那些平台模块比如java.sql、java.xml这些。一个容易混淆的点在 Java 层面这个加载器的parent字段是null。但它的委派逻辑仍然会把请求先交给 Bootstrap ClassLoader。这里的 “parent null” 只是表示在 Java 层面没有一个对应的 ClassLoader 对象作为父加载器不表示它不向上委派。我当时被这个搞混过看到 parent 是 null 以为它就是顶层了结果发现它还会向上抛给启动类加载器。3. 应用程序类加载器Application Class Loader你用得最多的这个加载器负责加载你的项目里写的类和引用的第三方 jar 包。具体来说就是 ClassPath类路径和模块路径上的所有类。你可以通过ClassLoader.getSystemClassLoader()拿到它。在 Java 9 里它的父加载器是 Platform Class Loader在 Java 8 里是 Extension Class Loader。平时写代码的时候你几乎感觉不到它的存在但你写的每个类的加载都是经过它的。4. 自定义类加载器Custom Class Loader给你自己玩的如果你觉得上面三层的加载方式满足不了你的需求你可以自己写一个 ClassLoader继承java.lang.ClassLoader就行。什么时候需要自己写类文件不在文件系统上而是从网络上下载类文件被加密了需要在加载时解密类文件存在数据库里你想对加载的字节码做增强比如一些 APM 工具就是这么做的我刚开始觉得自定义类加载器是八股文里才会出现的东西。后来用了一次 Spring Boot 的 DevTools发现它就是用不同的类加载器来隔离重新加载的类和原有的类才知道这东西在实际项目里真的在用。层级关系和双亲委派模型四层加载器的层级关系是这样的Bootstrap ClassLoader顶层C 实现 ↑ 委派 Platform / Extension ClassLoader ↑ 委派 Application ClassLoader ↑ 委派 Custom ClassLoader开发者自定义注意箭头的方向是向上委派不是向下查找。双亲委派到底是怎么工作的当一个类加载器收到加载一个类的请求时它不会自己先去尝试加载而是先把请求交给它的父加载器。父加载器又交给它的父加载器……一直交到最顶层的启动类加载器。启动类加载器尝试加载如果加载不到它返回我干不了然后下一级加载器再尝试。用一个具体的例子走一遍假设你要加载com.example.MyService。应用类加载器收到了请求。应用类加载器说“我先问问我爸爸能不能加载。”它把请求交给了平台类加载器Java 9 的话。平台类加载器也说“我再问问我爸爸。”平台类加载器把请求交给 Bootstrap ClassLoader。Bootstrap ClassLoader 尝试加载com.example.MyService。它只负责核心类库找不到这个类。Bootstrap ClassLoader 返回我找不到。平台类加载器自己尝试加载。它只负责平台模块也找不到。平台类加载器返回我也找不到。应用类加载器终于自己出手了。它在自己负责的 ClassPath 上找到了com.example.MyService加载成功。这个流程走下来你会发现问题每一层都要先问爸爸层层向上再层层向下。这不是很慢吗为什么非要这么设计双亲委派看起来绕但它的核心目的只有一个保证核心类的唯一性。如果不用双亲委派你可以在自己的代码里写一个java.lang.String然后用自己的类加载器加载。JVM 就分不清哪个是真正的 JDK 核心类哪个是你自己写的。这种混乱会带来安全性问题想象一下有人写了一个假的javax.crypto.Cipher在里面做手脚。双亲委派的解决方案是不管谁要加载java.lang.String请求都会一路向上传到 Bootstrap ClassLoader。Bootstrap ClassLoader 加载了真正的java.lang.String然后返回给下层。下层就不会再尝试用自己的版本了。这样就保证了全 JVM 范围内java.lang.String只有一份。我第一次理解这个设计意图的时候觉得还是挺巧妙的。用了一个简单的向上问的规则就解决了类重复和类安全两个问题。代价只是在加载过程中多走几层委派而类加载本身是低频操作这点性能损失可以忽略。双亲委派不是万能的但这个模型也有解决不了的问题。Java SPIService Provider Interface就是一个典型例子。SPI 的机制是定义一个接口比如java.sql.Driver接口在核心库里但实现类比如 MySQL 的驱动在第三方 jar 里。Bootstrap ClassLoader 加载了java.sql.Driver但当它尝试加载实现类的时候它找不到因为它根本不负责 ClassPath。Java 的解决方案是Thread.currentThread().getContextClassLoader()拿到当前线程的上下文类加载器通常是应用类加载器让爸爸用儿子的加载器去加载类。这就是所谓的打破双亲委派。Tomcat 这样的 Web 容器也要打破双亲委派。每个 Web 应用需要独立加载自己的类互不干扰。你部署了两个 war 包里面都有同名的类不能互相覆盖。Tomcat 的做法是给每个 Web 应用创建独立的类加载器优先自己加载加载不到才交给父加载器跟双亲委派的方向刚好反过来。不同版本之间的变化学习类加载器的时候Java 8 和 Java 9 的差异让我多花了不少时间。因为网上的很多博客还在讲 Java 8 的体系而我现在用的已经是 Java 17 了。项目Java 8 及以前Java 9 及以后核心类库jre/lib/rt.jar$JAVA_HOME/lib/modules模块化运行时镜像第二层加载器Extension Class LoaderPlatform Class Loader扩展目录jre/lib/ext已移除系统属性java.ext.dirs已移除最核心的变化是 Java 9 的模块化系统Project Jigsaw。它不仅改了类加载器的名字还改了类库的存储方式和访问权限。用旧的博客文章学新版本的 JVM确实需要小心配对版本。学完之后我觉得需要记住的把类加载器的体系过了一遍之后有几个体会比较深1. 分层是手段安全才是目的。类加载器的分层不是为了好看而是为了让核心类不被冒充。双亲委派用极简的向上问规则实现了这一点。2. null 不等于没有。Bootstrap ClassLoader 在 Java 代码里用 null 表示但它是真实存在的。在 Java 里看到getClassLoader()返回 null就知道这类的来头最大。3. 打破双亲委派不是 bug是 feature。SPI、Web 容器、热部署工具它们需要打破双亲委派不是因为模型设计得不好而是因为有些场景天然就不符合向上问的逻辑。框架需要自己决定怎么加载。4. 版本很重要。Java 9 改了类加载器的命名和结构连rt.jar都没了。如果还在看 Java 8 的博客学现在的 JVM有些地方需要对得上号。如果你也在学类加载器建议先动手写一个简单的自定义 ClassLoader。不用复杂覆写findClass()方法从文件系统或者网络加载一个类就行。做一遍之后你对委派和加载的理解会比只看文章深很多。