ClassLoader深度解剖:双亲委派、Tomcat类隔离、SPI与模块化

发布时间:2026/7/4 3:57:35
ClassLoader深度解剖:双亲委派、Tomcat类隔离、SPI与模块化 一个Spring Boot应用启动时打印了NoClassDefFoundError但同一个jar明明在classpath里。一个Tomcat上部署了两个应用各自依赖不同版本的Guava却没有类冲突。这一切都靠ClassLoader的两层设计——隔离机制和委托机制。这篇讲清楚ClassLoader的底层运作以及那些看似诡异的类加载问题是怎么产生的。一、双亲委派模型为什么说它是反直觉的1.1 标准模型Java的类加载请求不是从下往上找而是从上往下委托Bootstrap ClassLoader (C实现加载rt.jar) ↑ 委派 Extension ClassLoader (JDK 9 改名为 Platform ClassLoader) ↑ 委派 Application ClassLoader (加载classpath) ↑ 委派 用户自定义 ClassLoader当自定义ClassLoader要加载java.lang.String时它不是自己去加载而是先问Application ClassLoaderApplication ClassLoader再问ExtensionExtension再问BootstrapBootstrap一看String是我的→自己加载→一路返回为什么叫反直觉因为请求向上委托加载结果向下传递。你不会在自定义ClassLoader里加载到JDK核心类它们全被Bootstrap截胡了。这保证了java.lang.String永远是同一个类不管谁加载它。1.2 核心源码// java.lang.ClassLoader.loadClass() 简化版protectedClass?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){// 1. 检查是否已经加载过Class?cfindLoadedClass(name);if(cnull){try{// 2. 向上委派给父加载器if(parent!null){cparent.loadClass(name,false);}else{cfindBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){// 父加载器找不到}// 3. 父加载器没找到自己加载if(cnull){cfindClass(name);}}returnc;}}二、双亲委派是怎么被破坏的破坏双亲委派在业界是个被说烂的概念。实际有两个完全不同的场景2.1 场景一JDBC驱动加载SPI// 获取数据库连接JDK核心APIConnectionconnDriverManager.getConnection(url,user,pass);问题是DriverManager在rt.jar里由Bootstrap ClassLoader加载com.mysql.jdbc.Driver在应用classpath里Application ClassLoader才能加载Bootstrap ClassLoader根本看不到MySQL的Driver类解决方案ServiceLoaderSPI机制打破了向上委托。// DriverManager.getConnection() 内部// 注意这里用的是 Thread.currentThread().getContextClassLoader()ServiceLoaderDriverloadedDriversServiceLoader.load(Driver.class);// Thread Context ClassLoader 默认是 Application ClassLoader// 所以Bootstrap ClassLoader可以通过TCCL向下加载类关键点SPI允许核心API的类由Bootstrap加载通过线程上下文类加载器TCCL获取应用类路径下的实现类——这是对向上委托的第一次破坏。2.2 场景二Tomcat类隔离# Tomcat的类加载结构Common ClassLoader(加载Tomcat自身 shared libs)|├── Webapp1 ClassLoader(加载WEB-INF/lib/*, WEB-INF/classes/*)|双亲是 Common但优先自己加载|└── Webapp2 ClassLoader(加载WEB-INF/lib/*, WEB-INF/classes/*)双亲是 Common但优先自己加载Tomcat的WebappClassLoader重写了loadClass方法改变了加载顺序// Tomcat WebappClassLoader 的加载顺序 1. 自己找先检查自己是否已加载 2. 允许Webapp自己加载优先WEB-INF下的类 3. 再委派给父加载器这就是为什么两个Webapp可以用不同版本的Guava——每个Webapp自己的WebappClassLoader优先从WEB-INF/lib加载Guava互不影响。三、JDK 9模块化之后的改变JDK 9的模块化JPMS引入了一些新概念# 查看模块间依赖java--list-modules# 常见模块# java.base - 所有模块的基座java.lang, java.util, java.io...# java.sql - JDBC相关# java.xml - XML解析# jdk.unsupported - sun.misc.Unsafe对ClassLoader的影响// JDK 8及之前Class.forName(com.sun.internal.jndi.DNS);// JDK 9需要显式add-opens// java --add-opens java.base/com.sun.internal.jndiALL-UNNAMED核心变化维度JDK 8JDK 9Extension ClassLoader存在改为Platform ClassLoader不加载类核心类rt.jar拆分为几十个模块内部API访问默认可反射默认不可访问需–add-opensBootstrap加载器Java中不可见仍然是底层加载器生产影响如果你还在用JDK 8的反射访问JDK内部类比如sun.misc.Unsafe的某些路径迁移到JDK 17需要加--add-opens参数。四、实战定位类加载问题4.1 看一个类是从哪里加载的# 启动时打印所有类的加载来源-XX:TraceClassLoading# 输出# [Loaded java.lang.System from /jdk/lib/modules]# [Loaded com.example.MyClass from file:/app/classes/]4.2 运行时查看# arthas看一个类被哪个ClassLoader加载[arthas27654]classloader-chash-rjava.util.HashMap# 输出# class info: java.util.HashMap# classLoader: BootstrapClassLoader// 代码里直接看Class?clazzOrderService.class;System.out.println(clazz.getClassLoader());// 输出: jdk.internal.loader.ClassLoaders$AppClassLoaderxxxx4.3 经典异常分析NoClassDefFoundError vs ClassNotFoundException# ClassNotFoundException主动找类时找不到Class.forName(com.example.Driver)# → 抛出ClassNotFoundException# NoClassDefFoundError一个类在编译期存在、运行期没了# 比如 OrderService implements Serializable# 但OrderService的jar不在classpath里# → 抛出NoClassDefFoundError一条实用判断异常含义常见根因ClassNotFoundException加载时类不存在jar缺失或版本不对NoClassDefFoundError类曾经存在但现在找不到依赖的另一个类不可用LinkageError同一个jar被两个ClassLoader加载类冲突UnsupportedClassVersionError类版本高于JVM版本用高版本JDK编译了代码4.4 依赖冲突的终极解法# Maven查看完整的依赖树找到冲突mvn dependency:tree-Dverbose|grepguava# Gradlegradle dependencies--configurationcompileClasspath# JDK自带的工具可选的类隔离解决方案jlink --module-path /jmods --add-modules java.base--output/mini-jre五、总结双亲委派的核心价值是安全——确保JDK核心类不被篡改不被多个加载器重复加载SPIJDBC、JNDI等破坏了向上委托——通过Thread Context ClassLoader让Bootstrap加载的API能找到应用层的实现Tomcat WebappClassLoader破坏了优先委派——改为优先自己加载实现应用间类隔离JDK 9模块化后内部API不可反射访问——迁移前检查--add-opens需求NoClassDefFoundError和ClassNotFoundException定位思路不同——一个看jar版本一个看类被谁依赖