java类加载机制升职加薪之旅--重点笔记
⼀、快速梳理JAVA类加载机制
三句话总结JDK8的类加载机制: 1. 类缓存:每个类加载器对他加载过的类都有⼀个缓存。 2. 双亲委派:向上委托查找,向下委托加载。 3. 沙箱保护机制:不允许应⽤程序加载JDK内部的系统类。
public class LoaderDemo { public static String a ="aaa"; public static void main(String[] args) throws ClassNotFoundException { // ⽗⼦关系 AppClassLoader <- ExtClassLoader <- BootStrap Classloader ClassLoader cl1 = LoaderDemo.class.getClassLoader(); System.out.println("cl1 > " + cl1); System.out.println("parent of cl1 > " + cl1.getParent()); // BootStrap Classloader由C++开发,是JVM虚拟机的⼀部分,本身不是JAVA类。 System.out.println("grant parent of cl1 > " + cl1.getParent().getParent()); // String,Int等基础类由BootStrap Classloader加载。 ClassLoader cl2 = String.class.getClassLoader(); System.out.println("cl2 > " + cl2); System.out.println(cl1.loadClass("java.util.List").getClass().getClassLoader()); // java指令可以通过增加-verbose:class -verbose:gc 参数在启动时打印出类加载情况 // 这些参数来⾃于 sun.misc.Launcher 源码 // BootStrap Classloader,加载java基础类。 System.out.println("BootStrap ClassLoader加载⽬录:" + System.getProperty("sun.boot.class.path")); // Extention Classloader 加载⼀些扩展类。 可通过-D java.ext.dirs另⾏指定⽬录 System.out.println("Extention ClassLoader加载⽬录:" + System.getProperty("java.ext.dirs")); // AppClassLoader 加载CLASSPATH,应⽤下的Jar包。可通过-D java.class.path另⾏指定⽬录 System.out.println("AppClassLoader加载⽬录:" + System.getProperty("java.class.path")); } }

//类加载器的核⼼⽅法 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 每个类加载起对他加载过的类都有⼀个缓存,先去缓存中查看有没有加载过 Class<?> c = findLoadedClass(name); if (c == null) { //没有加载过,就⾛双亲委派,找⽗类加载器进⾏加载。 long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) {
// 父加载器全部找不到,进入下方自身加载逻辑 父类的findClass方法报异常,到catch里,然后执行下面自己的方法。 } if (c == null) { long t1 = System.nanoTime(); // ⽗类加载起没有加载过,就⾃⾏解析class⽂件加载。 上层所有加载器都找不到,自己执行 findClass 加载(向下回调的本质) c = findClass(name); sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } //这⼀段就是加载过程中的链接Linking部分,分为验证、准备,解析三个部分。 // 运⾏时加载类,默认是⽆法进⾏链接步骤的。 if (resolve) { resolveClass(c); } return c; } }

二 沙箱保护机制
private ProtectionDomain preDefineClass(String name, ProtectionDomain pd) { if (!checkName(name)) throw new NoClassDefFoundError("IllegalName: " + name); // 不允许加载核⼼类 if ((name != null) && name.startsWith("java.")) { throw new SecurityException ("Prohibited package name: " + name.substring(0, name.lastIndexOf('.')));
三 Linking链接过程

四 多个点总结
URL jarPath = new URL("file:/Users/roykingw/DevCode/ClassLoadDemo/out/artifacts/SalaryCaler_jar/SalaryCaler. jar"); URLClassLoader urlClassLoader = new URLClassLoader(new URL[] {jarPath});
2,⾃定义类加载器重写findclass和defindClass方法
3,例如IDEA中的JRebel插件,还有之前介绍过的Arthas。(热加载插件)
4,java的SPI机制
public class SPITest { public static void main(String[] args) { List<String> names = SpringFactoriesLoader.loadFactoryNames(ApplicationContextInitializer.class, null); names.forEach(System.out::println); System.out.println("=============="); List<ApplicationContextInitializer> applicationContextInitializers = SpringFactoriesLoader.loadFactories(ApplicationContextInitializer.class, null); applicationContextInitializers.forEach(System.out::println); } }
5,java --classpath 启动类 引入外部jar包。
Java 自定义类加载器完整教程
一、核心基础
1. 双亲委派模型
类加载器层级: BootstrapClassLoader(启动类加载器) → ExtClassLoader(扩展) → AppClassLoader(应用) → 自定义类加载器 加载规则:先委托父加载器加载,父加载器找不到,自己再加载。
2. 自定义类加载器两种方式
- 继承
ClassLoader:重写findClass(String name)(推荐,遵循双亲委派) - 继承
URLClassLoader:适合加载磁盘 / 网络 jar、class 文件
禁止直接重写
loadClass(),会破坏双亲委派。
3. 关键方法
loadClass(String name):入口,实现双亲委派逻辑(父类已写好)findClass(String name):父加载器找不到时回调,我们在此读取字节码defineClass(name, byte[] b, off, len):将字节数组转为 Class 对象(核心 API)
二、最简自定义类加载器(继承 ClassLoader)
步骤
- 新建类继承
ClassLoader - 重写
findClass,读取.class文件字节流 - 调用
defineClass生成 Class - 反射实例化测试
1. 待加载测试类(提前编译出 Test.class)
// Test.java public class Test { public void hello() { System.out.println("自定义类加载器加载的Test类"); } }
编译:javac Test.java 得到 Test.class 文件。
2. 自定义加载器代码
import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class MyClassLoader extends ClassLoader { // class文件存放目录 private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } /** * 父加载器找不到类时,执行该方法读取字节码 */ @Override protected Class<?> findClass(String className) throws ClassNotFoundException { byte[] classBytes = getClassBytes(className); if (classBytes == null) { throw new ClassNotFoundException("类不存在:" + className); } // 字节数组转Class对象 return defineClass(className, classBytes, 0, classBytes.length); } /** * 根据全类名读取磁盘上的.class字节数组 */ private byte[] getClassBytes(String className) { // 全类名替换路径分隔符:com.demo.Test → com/demo/Test.class String path = classPath + File.separator + className.replace(".", File.separator) + ".class"; File classFile = new File(path); if (!classFile.exists()) { return null; } try (FileInputStream fis = new FileInputStream(classFile); ByteArrayOutputStream bos = new ByteArrayOutputStream()) { byte[] buf = new byte[1024]; int len; while ((len = fis.read(buf)) != -1) { bos.write(buf, 0, len); } return bos.toByteArray(); } catch (IOException e) { e.printStackTrace(); return null; } } }
3. 测试调用
public class TestLoader { public static void main(String[] args) throws Exception { // Test.class所在文件夹路径 MyClassLoader loader = new MyClassLoader("D:/class"); // 加载全类名 Class<?> clazz = loader.loadClass("Test"); // 反射创建对象 Object obj = clazz.newInstance(); // 调用方法 clazz.getMethod("hello").invoke(obj); // 打印类加载器 System.out.println("类加载器:" + clazz.getClassLoader()); } }
三、破坏双亲委派模型(拓展)
打破方式两种:
1,重写 loadClass() 改写委托逻辑(不推荐,破坏安全);
2,使用线程上下文类加载器 ContextClassLoader(SPI 标准方案)
JDBC 驱动 (JDK 核心包没有数据库驱动实现,Bootstrap/App 加载不到,需要线程上下文类加载器打破双亲,由下层应用加载器加载驱动;)。
业务场景:热更新、加载自定义加密 class,需要优先自己加载,不委托父加载器。 重写 loadClass 方法:
@Override public Class<?> loadClass(String name) throws ClassNotFoundException { // 1. 先查询是否已经加载过 Class<?> c = findLoadedClass(name); if (c == null) { try { // 不委派父加载器,直接自己加载 c = findClass(name); } catch (ClassNotFoundException e) { // 自己找不到再交给父加载器 super.loadClass(name); } } return c; }
四、URLClassLoader(加载 jar / 网络 class,更常用)
适合加载本地 jar 包、网络远程 class,不用自己写 IO 读取字节码:
import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; public class URLLoaderDemo { public static void main(String[] args) throws Exception { // class文件/jar所在路径 URL url = new URL("file:D:/class/"); URLClassLoader loader = new URLClassLoader(new URL[]{url}); Class<?> clazz = loader.loadClass("Test"); clazz.getDeclaredConstructor().newInstance(); } }
五、加密 class 文件加载实战(典型场景)
需求:class 文件加密存放,自定义加载器读取后解密再调用defineClass 核心逻辑:
- 磁盘读取加密字节数组
- 自定义解密算法还原原始 class 字节
- 解密后的 byte 数组传入
defineClass生成类
@Override protected Class<?> findClass(String className) throws ClassNotFoundException { byte[] encryptBytes = getEncryptBytes(className); // 自定义解密方法 byte[] realBytes = decrypt(encryptBytes); return defineClass(className, realBytes, 0, realBytes.length); }
六、关键注意事项
- 双亲委派好处:防止核心类被篡改(如自定义
java.lang.String不会被加载)、类全局唯一。 - 类的唯一性:同一个类名 + 同一个类加载器才视为同一个 Class;不同加载器加载同名类是两个完全独立类,
instanceof判断为 false。 - 线程上下文类加载器:
Thread.currentThread().getContextClassLoader(),SPI 机制常用,打破双亲委派。 - 资源释放:
URLClassLoader使用完调用close()释放资源,避免文件句柄泄漏。 - 禁止自定义包名:不能加载
java.、sun.开头的类,安全机制直接拦截。
七、典型使用场景
- 热部署 / 热更新(Spring Boot devtools、Tomcat 热加载)
- 加密 / 自定义格式 class 文件加载
- 动态加载外部 jar 包(插件化架构)
- 远程加载网络 class
- 隔离不同版本依赖(容器、中间件)
SPI + 线程上下文类加载器 打破双亲委派源码解析(JDBC 驱动场景)
一、背景矛盾:为什么双亲委派加载不了 JDBC 驱动
java.sql.DriverManager、java.sql.Driver是 rt.jar 内的核心类,由 Bootstrap 类加载器加载;- MySQL、PostgreSQL 等数据库驱动(
com.mysql.cj.jdbc.Driver)是第三方 jar,放在应用 classpath,由 AppClassLoader 加载;
按照双亲委派模型: Bootstrap 加载的 DriverManager 想主动创建驱动实现类,但是 Bootstrap 看不到 AppClassLoader 管理的第三方 jar,上层加载器无法访问下层加载器的类,直接加载会抛 ClassNotFoundException。
解决方案:线程上下文类加载器 TCCL(Thread Context ClassLoader),反向拿到下层应用类加载器,实现 “上层代码调用下层类”,打破双亲委派单向向上的限制。
二、关键入口:DriverManager.getConnection () 源码流程
1. 静态块加载驱动(JDK8/9 通用逻辑)
static { loadInitialDrivers(); println("JDBC DriverManager initialized"); } private static void loadInitialDrivers() { String drivers; try { // 读取系统属性 jdbc.drivers drivers = AccessController.doPrivileged(new PrivilegedAction<String>() { public String run() { return System.getProperty("jdbc.drivers"); } }); } catch (Exception ex) { drivers = null; } // 核心:通过SPI加载META-INF/services/java.sql.Driver下的驱动实现 AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { // 重点:获取当前线程上下文类加载器 ClassLoader l = Thread.currentThread().getContextClassLoader(); // SPI加载器,传入TCCL ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class, l); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try { while (driversIterator.hasNext()) { driversIterator.next(); } } catch (Throwable t) { // ignore } return null; } }); // 加载系统属性配置的驱动类 if (drivers != null && !drivers.isEmpty()) { String[] driversList = drivers.split(":"); for (String aDriver : driversList) { try { // 这里Class.forName默认使用调用方类加载器(Bootstrap),老方式 Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); } catch (Exception ex) { println("DriverManager.Initialize: load failed: " + ex); } } } }
核心代码片段拆解
// 1. 获取线程上下文类加载器(默认是AppClassLoader) ClassLoader l = Thread.currentThread().getContextClassLoader(); // 2. SPI指定用TCCL去加载驱动实现类 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class, l);
Driver.class是 Bootstrap 加载的接口;- 传入的
l是下层 AppClassLoader; - ServiceLoader 会用下层加载器去扫描 classpath 下第三方 jar 里的
META-INF/services/java.sql.Driver文件,读取驱动全类名并实例化。
三、ServiceLoader.load 如何利用 TCCL 打破双亲
ServiceLoader 核心逻辑
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) { return new ServiceLoader<>(service, loader); }
内部迭代器 hasNext() / next() 逻辑:
- 使用传入的
loader(TCCL=AppClassLoader)读取 classpath 资源文件; - 解析文件中驱动实现类全限定名;
Class.forName(className, true, loader)—— 用下层 AppClassLoader 加载驱动类;
正常双亲是上层调下层不行,这里主动把下层加载器传给 SPI 工具类,让上层核心包代码主动委托下层加载器加载第三方实现,实现反向委派。
四、TCCL 是什么?为什么能打破双亲
1. 双亲委派单向限制
加载器父子关系单向:自定义 → App → Ext → Bootstrap 只能子委托父,父不能主动访问子加载器,上层看不到下层的类。
2. 线程上下文类加载器作用
每个 Thread 持有一个 contextClassLoader,默认初始化时赋值为应用类加载器 AppClassLoader。 API:
// 获取 ClassLoader tccl = Thread.currentThread().getContextClassLoader(); // 修改(框架热加载、插件场景常用) Thread.currentThread().setContextClassLoader(loader);
它相当于后门:让上层加载的代码,可以拿到一个下层加载器引用,反向加载下层类,绕开双亲向上委托的固定流程。
3. 两种委派对比
- 标准双亲委派:加载请求向上传递,父优先;父看不到子类资源;
- TCCL 反向委派:上层代码主动拿到子加载器,直接用子加载器加载第三方实现,先下后上,打破原有顺序。
五、老版本 Class.forName ("com.mysql.cj.jdbc.Driver") 原理补充
JDBC4.0 以前必须手动写:
Class.forName("com.mysql.cj.jdbc.Driver");
Class.forName(String className)无参重载,使用当前调用者的类加载器;- 如果在业务代码(App 加载器加载的类)中调用,加载器是 AppClassLoader,可以正常加载驱动;
- 如果在 rt.jar 核心类中直接调用 forName,加载器是 Bootstrap,会找不到驱动; 所以 JDBC4.0 之后引入 SPI+TCCL 自动注册驱动,无需手动 Class.forName。
六、驱动注册完整链路
- 应用启动,执行
DriverManager静态代码块; - 获取当前线程 TCCL(AppClassLoader);
- ServiceLoader 通过 AppClassLoader 扫描所有 jar 的
META-INF/services/java.sql.Driver; - 文件内写死
com.mysql.cj.jdbc.Driver; - AppClassLoader 加载驱动类,执行驱动静态代码块;
// MySQL驱动内部静态代码 static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Register fail"); } }
- 将驱动实例注册到 Bootstrap 加载的 DriverManager 内部集合;
- 后续
getConnection()遍历已注册驱动,匹配 url 获取连接。
七、总结核心要点
- 矛盾根源:Bootstrap 加载的标准接口,实现类在下层 App 加载器管理的第三方 jar,双亲单向访问不到;
- 破局关键:线程上下文类加载器 TCCL,给上层核心代码一个下层加载器的引用;
- 打破方式:上层主动传入子加载器给 ServiceLoader,由子加载器加载第三方实现类,反向委派;
- 适用场景:所有 SPI 场景(JDBC、日志 SLF4J、Dubbo SPI、Spring SPI)均依赖 TCCL 打破双亲委派;
- 区别于重写 loadClass 破坏双亲:TCCL 是 JDK 提供的标准、安全的反向加载方案,不会破坏全局类加载安全机制。

浙公网安备 33010602011771号