
类加载器与双亲委派模型上一篇我们梳理了类加载的完整生命周期其中的加载阶段有一个核心动作——通过类的全限定名获取定义此类的二进制字节流。这个动作由谁来完成答案就是类加载器ClassLoader。类加载器是JVM类加载机制的核心组件它不仅负责查找和加载字节流还决定了类的可见性和隔离性。本篇将深入讲解JVM中各类加载器的层次结构、双亲委派模型的设计原理以及如何自定义类加载器。类加载器的种类JVM默认提供了几种类加载器它们各自负责不同范围的类加载工作形成了一个层次结构。从JDK 8到JDK 11/17加载器的命名和职责有一些变化但核心架构一致。Bootstrap ClassLoader引导类加载器Bootstrap ClassLoader由C实现在HotSpot JVM中是JVM的一部分不是Java类。它负责加载JVM运行所需的核心类库即JAVA_HOME/lib目录下或者被-Xbootclasspath参数指定的路径中的类库。// 获取String类的类加载器System.out.println(String.class.getClassLoader());// 输出: null// null表示Bootstrap ClassLoader因为它不是Java对象注意由于Bootstrap ClassLoader由C实现Java层面无法直接获取到它的引用getClassLoader()返回null就代表该类由Bootstrap ClassLoader加载。Extension ClassLoader / Platform ClassLoader扩展类加载器在JDK 8中称为Extension ClassLoader扩展类加载器由sun.misc.Launcher$ExtClassLoader实现负责加载JAVA_HOME/lib/ext目录下或由java.ext.dirs系统变量指定路径中的类库。从JDK 9开始由于模块化系统Jigsaw的引入扩展类加载器被改名为Platform ClassLoader平台类加载器实现类变为jdk.internal.loader.ClassLoaders$PlatformClassLoader。它负责加载平台模块中的类。// JDK 8// sun.misc.Launcher$ExtClassLoaderxxxSystem.out.println(sun.misc.Launcher.getLauncher().getClassLoader().getParent());// JDK 11// jdk.internal.loader.ClassLoaders$PlatformClassLoaderxxxClassLoaderplatformLoaderClassLoader.getPlatformClassLoader();System.out.println(platformLoader);Application ClassLoader应用程序类加载器Application ClassLoader也称为System ClassLoader系统类加载器由sun.misc.Launcher$AppClassLoaderJDK 8或jdk.internal.loader.ClassLoaders$AppClassLoaderJDK 9实现。它负责加载用户类路径-classpath或-cp上的类库也就是我们自己编写的应用类和第三方依赖。// 获取系统类加载器即Application ClassLoaderClassLoaderappLoaderClassLoader.getSystemClassLoader();System.out.println(appLoader);// 输出类似: jdk.internal.loader.ClassLoaders$AppClassLoaderxxx自定义类加载器除了以上三种类加载器开发者还可以通过继承java.lang.ClassLoader类来实现自定义类加载器。自定义类加载器常见的应用场景包括从网络、加密文件、数据库等非标准来源加载类实现类的隔离如Web容器中不同应用加载同名类的不同版本实现热部署和热替换类加载器层次结构┌─────────────────────────┐ │ Bootstrap ClassLoader │ (C实现, 加载核心类库) │ java.lang.String 等 │ └────────────┬────────────┘ │ 父加载器 ┌────────────▼────────────┐ │ Extension/Platform CL │ (Java实现, 加载扩展模块) │ javax.*, javafx.* 等 │ └────────────┬────────────┘ │ 父加载器 ┌────────────▼────────────┐ │ Application ClassLoader │ (加载classpath) │ 用户类 第三方依赖 │ └────────────┬────────────┘ │ 父加载器 ┌────────────▼────────────┐ │ 自定义 ClassLoader │ (按需加载) │ 网络/加密/动态生成等 │ └─────────────────────────┘需要特别指出这里的父子关系不是继承关系而是组合关系通过parent字段引用。子加载器持有一个指向父加载器的引用形成委托链。双亲委派模型工作流程双亲委派模型Parent Delegation Model的工作流程非常简洁当一个类加载器收到了类加载请求时它首先不会自己去尝试加载这个类而是把这个请求委派给父类加载器去完成。每一个层次的类加载器都是如此因此所有的加载请求最终都应该传送到顶层的Bootstrap ClassLoader中。只有当父加载器反馈自己无法完成这个加载请求它的搜索范围中没有所需的类时子加载器才会尝试自己去加载。用伪代码描述这个逻辑protectedClass?loadClass(Stringname,booleanresolve){// 1. 检查类是否已被加载Class?cfindLoadedClass(name);if(cnull){try{// 2. 委托给父加载器加载if(parent!null){cparent.loadClass(name,false);}else{// 3. 父加载器为null说明到顶了使用Bootstrap加载器cfindBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){// 父加载器无法加载}if(cnull){// 4. 父加载器无法加载自己尝试加载cfindClass(name);}}returnc;}这段逻辑实际就是java.lang.ClassLoader的loadClass方法的核心实现。一个完整的加载流程示例假设我们编写了一个com.example.HelloWorld类放在classpath下。当JVM第一次需要使用这个类时加载流程如下1. Application ClassLoader 收到加载 com.example.HelloWorld 的请求 2. Application ClassLoader 委托给 Extension ClassLoader 3. Extension ClassLoader 委托给 Bootstrap ClassLoader 4. Bootstrap ClassLoader 在 JAVA_HOME/lib 中查找 → 未找到 → 返回null 5. Extension ClassLoader 在 JAVA_HOME/lib/ext 中查找 → 未找到 → 返回null 6. Application ClassLoader 在 classpath 中查找 → 找到HelloWorld.class → 加载成功设计意图双亲委派模型的设计有两个核心目的1. 安全性假设没有双亲委派模型用户可以自定义一个名为java.lang.String的类由Application ClassLoader先加载那么核心API就被篡改了。有了双亲委派模型java.lang.String的加载请求会一路委派到Bootstrap ClassLoader由它从核心类库中加载真正的String类用户的假String永远不会被加载。2. 避免重复加载同一个类被不同类加载器加载会产生不同的Class对象。双亲委派模型保证了Java核心类库只会被Bootstrap ClassLoader加载一次避免了重复加载和类型不一致的问题。Class对象的唯一性这里有一个非常重要的概念对于任何一个类都需要由加载它的类加载器和这个类本身一同确立其在JVM中的唯一性。换句话说比较两个类是否相等只有在这两个类是由同一个类加载器加载的前提下才有意义。publicclassClassIdentityDemo{publicstaticvoidmain(String[]args)throwsException{// 自定义两个不同的类加载器加载同一个class文件ClassLoaderloader1newMyClassLoader(/path/to/classes/);ClassLoaderloader2newMyClassLoader(/path/to/classes/);Class?class1loader1.loadClass(com.example.HelloWorld);Class?class2loader2.loadClass(com.example.HelloWorld);System.out.println(class1class2);// 输出: false// 虽然是同一个class文件但由不同类加载器加载产生不同的Class对象Objectobj1class1.newInstance();Objectobj2class2.newInstance();System.out.println(obj1instanceofcom.example.HelloWorld);// 输出: false —— 这就是类型隔离的本质// obj1的真实类型是 loader1加载的HelloWorld// instanceof检查的是 Application ClassLoader加载的HelloWorld// 两者不是同一个类}}这个特性是类加载器隔离的基础。Tomcat利用它实现Web应用之间的类隔离OSGi利用它实现模块化——这些内容我们将在后续文章中展开。自定义类加载器自定义类加载器通常只需要继承ClassLoader并重写findClass方法。如果不想破坏双亲委派模型不要重写loadClass只重写findClass。下面是一个完整的自定义类加载器示例它从指定磁盘路径加载class文件// 适用: JDK 8/11/17publicclassDiskClassLoaderextendsClassLoader{privateStringclassPath;// class文件所在目录publicDiskClassLoader(StringclassPath){// 不指定parent默认parent为当前线程的上下文类加载器// 通常就是Application ClassLoaderthis.classPathclassPath;}publicDiskClassLoader(StringclassPath,ClassLoaderparent){super(parent);this.classPathclassPath;}OverrideprotectedClass?findClass(Stringname)throwsClassNotFoundException{byte[]classDataloadClassData(name);if(classDatanull){thrownewClassNotFoundException(无法找到类: name);}// defineClass将字节数组转换为Class对象// 这是JVM提供的原生方法负责验证、解析等底层工作returndefineClass(name,classData,0,classData.length);}privatebyte[]loadClassData(Stringname){// 将类名转换为文件路径: com.example.HelloWorld → com/example/HelloWorld.classStringpathclassPath/name.replace(.,/).class;try{PathfilePaths.get(path);if(!Files.exists(file)){returnnull;// 返回null表示自己无法加载}returnFiles.readAllBytes(file);}catch(IOExceptione){returnnull;}}}使用自定义类加载器publicclassDiskClassLoaderDemo{publicstaticvoidmain(String[]args)throwsException{DiskClassLoaderloadernewDiskClassLoader(D:/custom-classes);// 加载com.example.HelloWorldClass?clazzloader.loadClass(com.example.HelloWorld);// 通过反射创建实例并调用方法Objectinstanceclazz.getDeclaredConstructor().newInstance();Methodmethodclazz.getMethod(sayHello);method.invoke(instance);// 查看加载器System.out.println(clazz.getClassLoader());// 输出: DiskClassLoaderxxx如果是自定义加载器加载的话// 如果D:/custom-classes/com/example/HelloWorld.class不存在// 由于双亲委派最终会由Application ClassLoader从classpath中加载// 输出将是: jdk.internal.loader.ClassLoaders$AppClassLoaderxxx}}自定义类加载器何时打破双亲委派如果自定义类加载器只重写了findClass它会遵循双亲委派模型——先委托父加载器加载父加载器加载不了才自己加载。但在某些场景下我们需要打破双亲委派比如隔离需求Tomcat的WebApp类加载器优先自己加载而不是先委托父加载器热部署需要重新加载已修改的类而父加载器中缓存的旧版本无法更新SPI机制父加载器需要访问子加载器的类下一篇详细讲解打破双亲委派的方法是重写loadClass方法publicclassCustomClassLoaderextendsClassLoader{privateStringclassPath;publicCustomClassLoader(StringclassPath,ClassLoaderparent){super(parent);this.classPathclassPath;}OverrideprotectedClass?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{// 1. 检查是否已加载Class?cfindLoadedClass(name);if(cnull){// 2. 对自己负责的类优先自己加载打破双亲委派if(name.startsWith(com.myapp.)){try{cfindClass(name);}catch(ClassNotFoundExceptione){// 自己加载失败再走父加载器}}// 3. 其他类走正常的双亲委派if(cnull){if(getParent()!null){cgetParent().loadClass(name);}else{thrownewClassNotFoundException(name);}}}if(resolve){resolveClass(c);}returnc;}OverrideprotectedClass?findClass(Stringname)throwsClassNotFoundException{byte[]dataloadClassData(name);if(datanull)thrownewClassNotFoundException(name);returndefineClass(name,data,0,data.length);}privatebyte[]loadClassData(Stringname){StringpathclassPath/name.replace(.,/).class;try{returnFiles.readAllBytes(Paths.get(path));}catch(IOExceptione){returnnull;}}}实践要点优先使用findClass而非loadClass除非你有明确的隔离需求否则始终重写findClass保持双亲委派。随意打破双亲委派可能导致核心API被篡改、类加载混乱等严重问题。defineClass的不可逆性一旦defineClass成功这个类的Class对象就会被锁定在该类加载器中。即使后续字节码文件被修改同一个类加载器再次defineClass同名类会抛出LinkageError。热部署必须创建新的类加载器实例。避免类加载器内存泄漏类加载器引用了大量Class对象如果类加载器无法被GC回收如线程的ContextClassLoader引用会导致Metaspace持续增长。在使用线程池时尤其需要注意。JDK 9模块化对类加载器的影响JDK 9引入模块化后类加载器之间增加了模块层的概念。java.lang.ClassLoader增加了getNamedModule()方法BootClassLoader的加载范围通过模块定义而非路径。但双亲委派模型的基本架构没有改变。-Xbootclasspath/a的使用如果需要将自定义的类加入引导类加载器的加载范围如覆盖或补充核心类可以使用-Xbootclasspath/a:/path/to/your.jar/a表示append追加到末尾。java-Xbootclasspath/a:./supplement.jar-jaryour-app.jar小结JVM类加载器分为Bootstrap ClassLoaderC实现加载核心库、Extension/Platform ClassLoader加载扩展模块、Application ClassLoader加载classpath以及自定义类加载器。双亲委派模型要求类加载请求先委派给父加载器父加载器无法加载时子加载器才自行加载。其设计目的是保证安全性和避免重复加载。类的唯一性由类加载器类名共同确定。同一个class文件被不同类加载器加载后产生不同的Class对象instanceof检查会返回false。自定义类加载器继承ClassLoader并重写findClass可保持双亲委派重写loadClass则可打破双亲委派。下一篇我们将探讨双亲委派模型的局限性深入SPI机制和线程上下文类加载器如何打破这一模型。