类加载器类加载器ClassLoader说白了只是个“搬运工”负责把磁盘上的.class字节码文件拉进内存而加载、验证、准备、解析、初始化则是 JVM 拿到文件后在内部进行的“组装、安检和激活”过程。类加载的生命周期加载验证、准备、解析连接初始化️ 1. 验证Verification—— 安全检查一句话总结检查拉进来的.class文件是不是合法的有没有坏人恶意篡改。为什么要这一步字节码文件不一定非要用 Java 编译器javac生成任何人都可以用二进制编辑器手动写一个.class文件。如果没有验证里面写了破坏 JVM 内存、攻击系统的恶意代码JVM 直接运行就瘫痪了。具体查什么文件格式验证检查开头是不是魔数0xCAFEBABE咖啡宝贝版本号是否在当前 JVM 接受范围内。元数据验证检查语法这个类有没有父类是不是继承了被final修饰的类字节码验证保证程序语义是合法的比如不会出现“把一个对象强转成毫无关系的另一个类”这种离谱操作。符号引用验证后面解析阶段会用确保能根据名字找到对应的类、方法和字段。储备阶段2. 准备Preparation—— 分配内存赋零值一句话总结为类的静态变量static 变量在方法区分配内存并设置默认初始值。核心细节面试常考点此时赋的值是“零值”如0、0.0、null、false而不是你在代码里写的那个值举个栗子假设你的类里写了这一行public static int value 123;在准备阶段过完后value在内存里的值是0而不是123真正的123要等到初始化阶段才会赋值。特殊情况常量如果是被final修饰的常量public static final int value 123;因为有了final它是不可变的。在编译时 javac 就为它生成了ConstantValue属性所以在准备阶段value就会直接被赋值为123。 3. 解析Resolution—— 符号引用转直接引用一句话总结把常量池内的“名字字符串标签”换成真正的“内存地址指针”。什么叫符号引用Symbolic References你在写代码或者写字节码时调用一个方法com.user.OrderService.string()。此时 JVM 内部并不知道这个方法具体在内存的哪个地方它只能用一串字符串符号来暂时代替“喏我以后要调用这个名字的方法”。什么叫直接引用Direct References解析阶段开始后JVM 在内存里一查找到了OrderService.string()对应的真实内存起始地址比如0x7fff1234。然后把之前的字符串名字替换成这个真实的内存指针。解析的对象类或接口、字段、类方法、接口方法等。 4. 初始化Initialization—— 真正执行 Java 代码一句话总结这是类加载的最后一步JVM 开始真正执行你在类里写的 Java 赋值语句和静态代码块。核心底层初始化阶段本质上就是 JVM 自动收集类里所有静态变量的赋值动作和静态代码块static {}融合成一个叫做clinit()Class Initialize的方法然后去执行它。再看刚才的栗子public static int value 123;到了初始化阶段JVM 执行clinit()方法value的值才真正从0变成了123。触发初始化的时机主动引用类不是平白无故初始化的只有遇到以下情况部分常见情况才会触发使用new关键字实例化对象。读取或设置一个类的静态字段被 final 修饰的常量除外。调用一个类的静态方法。使用Class.forName(...)反射加载类。虚拟机启动时包含main()方法那个主类会被率先初始化。 总结通关图谱为了让你在学接下来的“三种类加载器”时完全不晕记住这个连贯的故事加载搬运工ClassLoader把.class二进制流读进内存。验证安检员上场看看文件有没有被下毒防篡改。准备木匠在内存方法区盖好静态变量的“毛坯房”里面先刷上白墙赋零值0。解析导航仪把代码里的“路标字符串”换成精细的“GPS 经纬度内存直接引用”。初始化装修队进场把精装家具搬进去按照你的代码意图给静态变量正式赋值执行clinit()。这五步走完这个类在 JVM 里就彻底“活了”接下来你就可以愉快地new它的对象实例了。把这套流程焊死在脑子里再去学双亲委派和三大类加载器逻辑就会顺畅得不可思议三大类加载器名称加载哪的类说明Bootstrap ClassLoader启动类加载器JAVA_HOME/jre/lib目录下的核心类库如rt.jar、charsets.jar等 JDK 核心类JVM 底层 C 实现无 Java 类实例Java 代码中无法直接访问getClassLoader()会返回nullExtension ClassLoader扩展类加载器JAVA_HOME/jre/lib/ext目录下的扩展 jar 包父加载器为 Bootstrap ClassLoaderJava 代码中获取其上级加载器时会显示为nullApplication ClassLoader应用 / 系统类加载器项目classpath路径下的自定义类、第三方依赖 jar 包父加载器为 Extension ClassLoader是 Java 代码中默认的类加载器自定义类加载器自定义路径如网络、加密文件、特殊目录等下的类父加载器默认指定为 Application ClassLoader可实现自定义类加载逻辑1. 启动类加载器 (Bootstrap ClassLoader) —— “始祖级大佬”它在哪、加载啥负责加载 Java 核心类库也就是你配置的JAVA_HOME/jre/lib目录下的核心 jar 包比如最关键的rt.jar里面躺着java.lang.Object、java.lang.String、java.util.HashMap等。底层硬核秘密面试爱考它不是用 Java 语言写的它是用C/C写的嵌套在 JVM 内核里面。它没有实例因为它不是一个 Java 对象所以你在 Java 代码里如果尝试去获取它返回的结果永远是null。举个栗子如果你执行String.class.getClassLoader()你会发现打印出来的是null。表里说明写的“显示为 null”就是这个意思。2. 扩展类加载器 (Extension ClassLoader) —— “皇家护卫”它在哪、加载啥负责加载 Java 的扩展类库对应的目录是JAVA_HOME/jre/lib/ext。这里面放的是一些官方自带但不是最核心的扩展工具 jar 包。底层秘密它是由 Java 语言编写的具体类名是sun.misc.Launcher$ExtClassLoader。表里写着“上级为 Bootstrap”意思是它的逻辑父加载器是 Bootstrap。3. 应用程序类加载器 (Application ClassLoader) —— “搬砖主力军”它在哪、加载啥负责加载classpath类路径下的所有类。说白了你在项目里自己写的代码、引入的第三方 Maven 依赖如 Spring、MyBatis、或者是各类 jar 包全部都是由它来负责加载进内存的。底层秘密它也是 Java 写的sun.misc.Launcher$AppClassLoader。因为平时我们绝大多数类都是它加载的所以它也叫系统类加载器System ClassLoader。如果你写一个自己的类User.class.getClassLoader()打印出来的就是它。4. 自定义类加载器 (Custom ClassLoader) —— “特种兵”为什么需要它官方的前三个加载器只能去本地固定的磁盘目录或环境变量里加载明文的.class文件。如果我的业务场景很特殊呢比如我的字节码文件是加密过的防止别人反编译需要加载时在内存里先解密。比如我的.class文件不在本地而是存在远端服务器或数据库里需要通过网络网络请求读进来。想加载非classpath随意路径中的类文件。怎么做继承java.lang.ClassLoader类重写findClass()方法你就能自己手写一个属于你的类加载器。⚠️ 盯紧图里最右侧的“说明”这是在给双亲委派埋伏笔注意看图里写的Extension 的上级是 BootstrapApplication 的上级是 Extension自定义类加载器的上级是 Application纠错警示灯这里的“上级”绝对不是面向对象里的“继承extends”关系在 JVM 源码里它们之间既没有AppClassLoader extends ExtClassLoader也没有ExtClassLoader extends Bootstrap。 它们是通过组合Combination关系来维持组合的。也就是说每个 ClassLoader 实例里面都有一个成员变量叫parentAppClassLoader的parent属性指向了ExtClassLoader实例。这种“逐级引向上级”的链条就是接下来你要学的双亲委派模型Parent Delegation Model的核心骨架双亲委派模式就是调用类加载器的loadClass方法时 查找类的规则。protectClass?loadClass(Stirngname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){// 1.检查该类是否已经加载Class?cfindLoadedClass(name);if(cnull){longt0System.nanoTime();try{if(parent!null){// 2.有上级的话 委派上级 loadClasscparent.loadClass(name,false);}else{// 3.没有上级了ExtClassLoader), 则委派 BootstrapClassLoadercfindBootstrapClassOrNull(name);}}catch(){}if(cnull){longt1System.nanoTime();// 4.每一层找不到调用findClass 方法每个类加载器自己扩展来加载cfindClass(anme);}}}}线程上下文类加载器线程上下文类加载器Thread Context ClassLoader以及它如何用来打破双亲委派模型。一个很好的引子在老版本的 JDBC 中我们需要手动写Class.forName(com.mysql.jdbc.Driver)来加载驱动但在 JDBC 4.0 之后即使不写这行代码MySQL 驱动也能被正确加载。这背后其实隐藏着一个著名的设计矛盾也就是SPIService Provider Interface机制。1. 痛点双亲委派模型的“死穴”在正常情况下双亲委派模型要求如果一个类由某个类加载器加载那么它里面引用的其他类默认也会用同一个类加载器去加载。DriverManager的身份它是 JDK 核心类库的一部分位于java.sql包下因此它是由最顶层的Bootstrap ClassLoader启动类加载器加载的。MySQL 驱动的身份它是第三方厂商提供的 Jar 包位于classpath下原本应该由底层的App ClassLoader系统类加载器来加载。当DriverManager初始化并尝试去加载各个厂商实现的驱动时矛盾就来了DriverManager由 Bootstrap 加载想要调用底层的com.mysql.jdbc.Driver在 classpath 中Bootstrap 根本找不到、也管不着。这就是双亲委派模型的局限性核心类库顶层加载器无法直接访问用户代码底层加载器。2. 破局者线程上下文类加载器Thread Context ClassLoader为了解决这个“套娃”死结Java 引入了线程上下文类加载器。它相当于给顶层加载器开了一个“后门”允许顶层代码“逆向”调用底层的加载器。虽然图片在loadInitialDrivers()这里戛然而止如果我们追进这个方法的源码就会发现它的核心实现逻辑如下private static void loadInitialDrivers() { // 1. 使用 ServiceLoader 机制加载驱动 ServiceLoaderDriver loadedDrivers ServiceLoader.load(Driver.class); IteratorDriver driversIterator loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); // 这里会触发驱动类的加载和注册 } } catch(Throwable t) { // Do nothing } }当我们再进一步点进ServiceLoader.load(Driver.class)的源码时狐狸尾巴就露出来了public static S ServiceLoaderS load(ClassS service) { // 核心获取了当前线程的上下文类加载器是当前线程使用的类加载器默认就是Application classloader ClassLoader cl Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }3. 核心总结默认委派失败DriverManager是被BootstrapClassLoader加载的它在自己的管辖范围内JDK 核心库找不到引入的 MySQL 驱动包。借刀杀人打破委派每个 Java 线程在创建时默认都会把AppClassLoader设置为自己的ContextClassLoader上下文类加载器。成功加载DriverManager巧妙地通过Thread.currentThread().getContextClassLoader()拿到了这个AppClassLoader并强行用它去加载了classpath下的 MySQL 驱动。这种由高层加载器委托底层加载器去加载类的行为本质上打破了双亲委派模型那种只能“自底向上委派”的固有规则。自定义类加载器一、 为什么要自定义类加载器图片中列举了三个核心场景它们在实际开发尤其是中间件和服务器开发中非常经典加载非classpath随意路径中的类文件大白话默认的AppClassLoader只能加载项目环境变量classpath路径下的类。如果你想从网络上比如 RPC 远程传输的字节码、数据库里、或者服务器的某个特定目录如图片中的E:\myclasspath动态加载.class文件默认的加载器就无能为力了。通过接口解耦常用于框架设计、插件化大白话比如做插件化开发OSGi 架构。主程序只定义接口具体的实现类由不同的自定义类加载器动态从插件包里加载实现热插拔和完美解耦。类隔离不同应用的同名类都可以加载不冲突大白话这是Tomcat 等 Web 容器的看家本领。假设一个 Tomcat 里面同时运行了两个 Web 应用应用 A 用的是 Spring 4.0应用 B 用的是 Spring 5.0。如果不做隔离由于全限定名完全一样JVM 只会加载其中一个另一个应用直接崩溃。Tomcat 通过为每个 Web 应用分配一个独立的自定义类加载器完美实现了同名类的物理隔离。二、 核心步骤与底层源码的闭环写自定义类加载器的“黄金法则”1.继承ClassLoader 父类2.要遵从双亲委派机制,重写findClass方法■ 注意不是重写loadClass方法,否则不会走双亲委派机制3.读取类文件的字节码4.调用父类的defineClass方法来加载类5.使用者调用该类加载器的loadClass 方法特别是第 2 步点出了无数初学者最容易踩的坑。为什么是重写findClass而不是loadClass还记得你在双亲委派模式看到的loadClass源码吗它的逻辑是检查是否加载过。委派给父类加载器实现双亲委派机制。如果父类加载器找不到才调用findClass。关键点JDK 的设计者已经把双亲委派的模板流程在loadClass方法里写死了这就是模板方法模式。如果你重写了loadClass你就把双亲委派机制给无意间破坏了。如果你重写findClass当父类加载器找不到这个类时JVM 才会乖乖调用你重写的findClass去你指定的路径比如E:\myclasspath读入字节码。这样既实现了自定义加载又完美保留了双亲委派机制。三、 自定义类加载器标准代码模板下面是一个严格按照图片 5 个步骤实现的标准代码模板import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; // 步骤 1继承 ClassLoader 父类 public class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath classPath; } // 步骤 2重写 findClass 方法遵循双亲委派 Override protected Class? findClass(String name) throws ClassNotFoundException { // 步骤 3读取类文件的字节码 byte[] data loadClassData(name); if (data null) { throw new ClassNotFoundException(); } // 步骤 4调用父类的 defineClass 方法将字节数组转化为 Class 对象 return defineClass(name, data, 0, data.length); } // 步骤 3 的具体实现从物理磁盘读取 .class 文件为字节数组 private byte[] loadClassData(String className) { // 将包名 com.example.User 转换为路径 com/example/User.class String fileName classPath className.replace(., /) .class; try (FileInputStream ins new FileInputStream(fileName); ByteArrayOutputStream baos new ByteArrayOutputStream()) { int b; while ((b ins.read()) ! -1) { baos.write(b); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } }步骤 5使用者如何调用public class Test { public static void main(String[] args) throws Exception { // 1. 创建自定义类加载器指定去 E:\myclasspath 找类 MyClassLoader loader new MyClassLoader(E:/myclasspath/); // 2. 调用 loadClass 方法它内部会遵从双亲委派最终触发我们重写的 findClass Class? clazz loader.loadClass(com.example.MyMapImplementation); // 3. 实例化对象并使用 Object instance clazz.getDeclaredConstructor().newInstance(); System.out.println(类加载器是 clazz.getClassLoader()); // 输出将是MyClassLoaderxxxxx } }
JVM 类加载机制详解(生命周期・双亲委派・自定义加载器)
发布时间:2026/5/25 23:26:31
类加载器类加载器ClassLoader说白了只是个“搬运工”负责把磁盘上的.class字节码文件拉进内存而加载、验证、准备、解析、初始化则是 JVM 拿到文件后在内部进行的“组装、安检和激活”过程。类加载的生命周期加载验证、准备、解析连接初始化️ 1. 验证Verification—— 安全检查一句话总结检查拉进来的.class文件是不是合法的有没有坏人恶意篡改。为什么要这一步字节码文件不一定非要用 Java 编译器javac生成任何人都可以用二进制编辑器手动写一个.class文件。如果没有验证里面写了破坏 JVM 内存、攻击系统的恶意代码JVM 直接运行就瘫痪了。具体查什么文件格式验证检查开头是不是魔数0xCAFEBABE咖啡宝贝版本号是否在当前 JVM 接受范围内。元数据验证检查语法这个类有没有父类是不是继承了被final修饰的类字节码验证保证程序语义是合法的比如不会出现“把一个对象强转成毫无关系的另一个类”这种离谱操作。符号引用验证后面解析阶段会用确保能根据名字找到对应的类、方法和字段。储备阶段2. 准备Preparation—— 分配内存赋零值一句话总结为类的静态变量static 变量在方法区分配内存并设置默认初始值。核心细节面试常考点此时赋的值是“零值”如0、0.0、null、false而不是你在代码里写的那个值举个栗子假设你的类里写了这一行public static int value 123;在准备阶段过完后value在内存里的值是0而不是123真正的123要等到初始化阶段才会赋值。特殊情况常量如果是被final修饰的常量public static final int value 123;因为有了final它是不可变的。在编译时 javac 就为它生成了ConstantValue属性所以在准备阶段value就会直接被赋值为123。 3. 解析Resolution—— 符号引用转直接引用一句话总结把常量池内的“名字字符串标签”换成真正的“内存地址指针”。什么叫符号引用Symbolic References你在写代码或者写字节码时调用一个方法com.user.OrderService.string()。此时 JVM 内部并不知道这个方法具体在内存的哪个地方它只能用一串字符串符号来暂时代替“喏我以后要调用这个名字的方法”。什么叫直接引用Direct References解析阶段开始后JVM 在内存里一查找到了OrderService.string()对应的真实内存起始地址比如0x7fff1234。然后把之前的字符串名字替换成这个真实的内存指针。解析的对象类或接口、字段、类方法、接口方法等。 4. 初始化Initialization—— 真正执行 Java 代码一句话总结这是类加载的最后一步JVM 开始真正执行你在类里写的 Java 赋值语句和静态代码块。核心底层初始化阶段本质上就是 JVM 自动收集类里所有静态变量的赋值动作和静态代码块static {}融合成一个叫做clinit()Class Initialize的方法然后去执行它。再看刚才的栗子public static int value 123;到了初始化阶段JVM 执行clinit()方法value的值才真正从0变成了123。触发初始化的时机主动引用类不是平白无故初始化的只有遇到以下情况部分常见情况才会触发使用new关键字实例化对象。读取或设置一个类的静态字段被 final 修饰的常量除外。调用一个类的静态方法。使用Class.forName(...)反射加载类。虚拟机启动时包含main()方法那个主类会被率先初始化。 总结通关图谱为了让你在学接下来的“三种类加载器”时完全不晕记住这个连贯的故事加载搬运工ClassLoader把.class二进制流读进内存。验证安检员上场看看文件有没有被下毒防篡改。准备木匠在内存方法区盖好静态变量的“毛坯房”里面先刷上白墙赋零值0。解析导航仪把代码里的“路标字符串”换成精细的“GPS 经纬度内存直接引用”。初始化装修队进场把精装家具搬进去按照你的代码意图给静态变量正式赋值执行clinit()。这五步走完这个类在 JVM 里就彻底“活了”接下来你就可以愉快地new它的对象实例了。把这套流程焊死在脑子里再去学双亲委派和三大类加载器逻辑就会顺畅得不可思议三大类加载器名称加载哪的类说明Bootstrap ClassLoader启动类加载器JAVA_HOME/jre/lib目录下的核心类库如rt.jar、charsets.jar等 JDK 核心类JVM 底层 C 实现无 Java 类实例Java 代码中无法直接访问getClassLoader()会返回nullExtension ClassLoader扩展类加载器JAVA_HOME/jre/lib/ext目录下的扩展 jar 包父加载器为 Bootstrap ClassLoaderJava 代码中获取其上级加载器时会显示为nullApplication ClassLoader应用 / 系统类加载器项目classpath路径下的自定义类、第三方依赖 jar 包父加载器为 Extension ClassLoader是 Java 代码中默认的类加载器自定义类加载器自定义路径如网络、加密文件、特殊目录等下的类父加载器默认指定为 Application ClassLoader可实现自定义类加载逻辑1. 启动类加载器 (Bootstrap ClassLoader) —— “始祖级大佬”它在哪、加载啥负责加载 Java 核心类库也就是你配置的JAVA_HOME/jre/lib目录下的核心 jar 包比如最关键的rt.jar里面躺着java.lang.Object、java.lang.String、java.util.HashMap等。底层硬核秘密面试爱考它不是用 Java 语言写的它是用C/C写的嵌套在 JVM 内核里面。它没有实例因为它不是一个 Java 对象所以你在 Java 代码里如果尝试去获取它返回的结果永远是null。举个栗子如果你执行String.class.getClassLoader()你会发现打印出来的是null。表里说明写的“显示为 null”就是这个意思。2. 扩展类加载器 (Extension ClassLoader) —— “皇家护卫”它在哪、加载啥负责加载 Java 的扩展类库对应的目录是JAVA_HOME/jre/lib/ext。这里面放的是一些官方自带但不是最核心的扩展工具 jar 包。底层秘密它是由 Java 语言编写的具体类名是sun.misc.Launcher$ExtClassLoader。表里写着“上级为 Bootstrap”意思是它的逻辑父加载器是 Bootstrap。3. 应用程序类加载器 (Application ClassLoader) —— “搬砖主力军”它在哪、加载啥负责加载classpath类路径下的所有类。说白了你在项目里自己写的代码、引入的第三方 Maven 依赖如 Spring、MyBatis、或者是各类 jar 包全部都是由它来负责加载进内存的。底层秘密它也是 Java 写的sun.misc.Launcher$AppClassLoader。因为平时我们绝大多数类都是它加载的所以它也叫系统类加载器System ClassLoader。如果你写一个自己的类User.class.getClassLoader()打印出来的就是它。4. 自定义类加载器 (Custom ClassLoader) —— “特种兵”为什么需要它官方的前三个加载器只能去本地固定的磁盘目录或环境变量里加载明文的.class文件。如果我的业务场景很特殊呢比如我的字节码文件是加密过的防止别人反编译需要加载时在内存里先解密。比如我的.class文件不在本地而是存在远端服务器或数据库里需要通过网络网络请求读进来。想加载非classpath随意路径中的类文件。怎么做继承java.lang.ClassLoader类重写findClass()方法你就能自己手写一个属于你的类加载器。⚠️ 盯紧图里最右侧的“说明”这是在给双亲委派埋伏笔注意看图里写的Extension 的上级是 BootstrapApplication 的上级是 Extension自定义类加载器的上级是 Application纠错警示灯这里的“上级”绝对不是面向对象里的“继承extends”关系在 JVM 源码里它们之间既没有AppClassLoader extends ExtClassLoader也没有ExtClassLoader extends Bootstrap。 它们是通过组合Combination关系来维持组合的。也就是说每个 ClassLoader 实例里面都有一个成员变量叫parentAppClassLoader的parent属性指向了ExtClassLoader实例。这种“逐级引向上级”的链条就是接下来你要学的双亲委派模型Parent Delegation Model的核心骨架双亲委派模式就是调用类加载器的loadClass方法时 查找类的规则。protectClass?loadClass(Stirngname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){// 1.检查该类是否已经加载Class?cfindLoadedClass(name);if(cnull){longt0System.nanoTime();try{if(parent!null){// 2.有上级的话 委派上级 loadClasscparent.loadClass(name,false);}else{// 3.没有上级了ExtClassLoader), 则委派 BootstrapClassLoadercfindBootstrapClassOrNull(name);}}catch(){}if(cnull){longt1System.nanoTime();// 4.每一层找不到调用findClass 方法每个类加载器自己扩展来加载cfindClass(anme);}}}}线程上下文类加载器线程上下文类加载器Thread Context ClassLoader以及它如何用来打破双亲委派模型。一个很好的引子在老版本的 JDBC 中我们需要手动写Class.forName(com.mysql.jdbc.Driver)来加载驱动但在 JDBC 4.0 之后即使不写这行代码MySQL 驱动也能被正确加载。这背后其实隐藏着一个著名的设计矛盾也就是SPIService Provider Interface机制。1. 痛点双亲委派模型的“死穴”在正常情况下双亲委派模型要求如果一个类由某个类加载器加载那么它里面引用的其他类默认也会用同一个类加载器去加载。DriverManager的身份它是 JDK 核心类库的一部分位于java.sql包下因此它是由最顶层的Bootstrap ClassLoader启动类加载器加载的。MySQL 驱动的身份它是第三方厂商提供的 Jar 包位于classpath下原本应该由底层的App ClassLoader系统类加载器来加载。当DriverManager初始化并尝试去加载各个厂商实现的驱动时矛盾就来了DriverManager由 Bootstrap 加载想要调用底层的com.mysql.jdbc.Driver在 classpath 中Bootstrap 根本找不到、也管不着。这就是双亲委派模型的局限性核心类库顶层加载器无法直接访问用户代码底层加载器。2. 破局者线程上下文类加载器Thread Context ClassLoader为了解决这个“套娃”死结Java 引入了线程上下文类加载器。它相当于给顶层加载器开了一个“后门”允许顶层代码“逆向”调用底层的加载器。虽然图片在loadInitialDrivers()这里戛然而止如果我们追进这个方法的源码就会发现它的核心实现逻辑如下private static void loadInitialDrivers() { // 1. 使用 ServiceLoader 机制加载驱动 ServiceLoaderDriver loadedDrivers ServiceLoader.load(Driver.class); IteratorDriver driversIterator loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); // 这里会触发驱动类的加载和注册 } } catch(Throwable t) { // Do nothing } }当我们再进一步点进ServiceLoader.load(Driver.class)的源码时狐狸尾巴就露出来了public static S ServiceLoaderS load(ClassS service) { // 核心获取了当前线程的上下文类加载器是当前线程使用的类加载器默认就是Application classloader ClassLoader cl Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); }3. 核心总结默认委派失败DriverManager是被BootstrapClassLoader加载的它在自己的管辖范围内JDK 核心库找不到引入的 MySQL 驱动包。借刀杀人打破委派每个 Java 线程在创建时默认都会把AppClassLoader设置为自己的ContextClassLoader上下文类加载器。成功加载DriverManager巧妙地通过Thread.currentThread().getContextClassLoader()拿到了这个AppClassLoader并强行用它去加载了classpath下的 MySQL 驱动。这种由高层加载器委托底层加载器去加载类的行为本质上打破了双亲委派模型那种只能“自底向上委派”的固有规则。自定义类加载器一、 为什么要自定义类加载器图片中列举了三个核心场景它们在实际开发尤其是中间件和服务器开发中非常经典加载非classpath随意路径中的类文件大白话默认的AppClassLoader只能加载项目环境变量classpath路径下的类。如果你想从网络上比如 RPC 远程传输的字节码、数据库里、或者服务器的某个特定目录如图片中的E:\myclasspath动态加载.class文件默认的加载器就无能为力了。通过接口解耦常用于框架设计、插件化大白话比如做插件化开发OSGi 架构。主程序只定义接口具体的实现类由不同的自定义类加载器动态从插件包里加载实现热插拔和完美解耦。类隔离不同应用的同名类都可以加载不冲突大白话这是Tomcat 等 Web 容器的看家本领。假设一个 Tomcat 里面同时运行了两个 Web 应用应用 A 用的是 Spring 4.0应用 B 用的是 Spring 5.0。如果不做隔离由于全限定名完全一样JVM 只会加载其中一个另一个应用直接崩溃。Tomcat 通过为每个 Web 应用分配一个独立的自定义类加载器完美实现了同名类的物理隔离。二、 核心步骤与底层源码的闭环写自定义类加载器的“黄金法则”1.继承ClassLoader 父类2.要遵从双亲委派机制,重写findClass方法■ 注意不是重写loadClass方法,否则不会走双亲委派机制3.读取类文件的字节码4.调用父类的defineClass方法来加载类5.使用者调用该类加载器的loadClass 方法特别是第 2 步点出了无数初学者最容易踩的坑。为什么是重写findClass而不是loadClass还记得你在双亲委派模式看到的loadClass源码吗它的逻辑是检查是否加载过。委派给父类加载器实现双亲委派机制。如果父类加载器找不到才调用findClass。关键点JDK 的设计者已经把双亲委派的模板流程在loadClass方法里写死了这就是模板方法模式。如果你重写了loadClass你就把双亲委派机制给无意间破坏了。如果你重写findClass当父类加载器找不到这个类时JVM 才会乖乖调用你重写的findClass去你指定的路径比如E:\myclasspath读入字节码。这样既实现了自定义加载又完美保留了双亲委派机制。三、 自定义类加载器标准代码模板下面是一个严格按照图片 5 个步骤实现的标准代码模板import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.IOException; // 步骤 1继承 ClassLoader 父类 public class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath classPath; } // 步骤 2重写 findClass 方法遵循双亲委派 Override protected Class? findClass(String name) throws ClassNotFoundException { // 步骤 3读取类文件的字节码 byte[] data loadClassData(name); if (data null) { throw new ClassNotFoundException(); } // 步骤 4调用父类的 defineClass 方法将字节数组转化为 Class 对象 return defineClass(name, data, 0, data.length); } // 步骤 3 的具体实现从物理磁盘读取 .class 文件为字节数组 private byte[] loadClassData(String className) { // 将包名 com.example.User 转换为路径 com/example/User.class String fileName classPath className.replace(., /) .class; try (FileInputStream ins new FileInputStream(fileName); ByteArrayOutputStream baos new ByteArrayOutputStream()) { int b; while ((b ins.read()) ! -1) { baos.write(b); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } }步骤 5使用者如何调用public class Test { public static void main(String[] args) throws Exception { // 1. 创建自定义类加载器指定去 E:\myclasspath 找类 MyClassLoader loader new MyClassLoader(E:/myclasspath/); // 2. 调用 loadClass 方法它内部会遵从双亲委派最终触发我们重写的 findClass Class? clazz loader.loadClass(com.example.MyMapImplementation); // 3. 实例化对象并使用 Object instance clazz.getDeclaredConstructor().newInstance(); System.out.println(类加载器是 clazz.getClassLoader()); // 输出将是MyClassLoaderxxxxx } }