jvm 将不同类加载器加载的同名类视为完全独立的类,即使字节码相同也无法强转、共享静态变量或调用私有方法;典型问题包括 classcastexception 和静态变量隔离,常见于 osgi、tomcat、spring boot devtools 等场景。

同一个类名被不同类加载器加载,JVM 会视为完全不同的类
不会报错,也不会自动合并或共享;ClassLoader A 加载的 com.example.Foo 和 ClassLoader B 加载的同名类,在 JVM 内部是两个独立的 java.lang.Class 实例。哪怕字节码一模一样,它们之间也无法强转、无法共用静态变量、无法互相调用私有方法。
- 典型现象:
ClassCastException出现在看似合理的转型处,比如(Foo) obj,但obj.getClass().getClassLoader()和当前线程上下文类加载器不一致 - 常见于 OSGi、Spring Boot DevTools、Tomcat 的 webapp 类加载隔离、自定义
URLClassLoader场景 - 判断方式:打印
obj.getClass().getName() + "@" + obj.getClass().getClassLoader(),看是否指向不同加载器实例
Class.forName(String) 默认使用调用者的类加载器,不是系统类加载器
很多人误以为它总走 ClassLoader.getSystemClassLoader(),其实它用的是当前执行该方法的类的 getClassLoader() —— 即「发起调用的类」所属的加载器。这在嵌套类加载(如 SPI 加载、插件模块)中极易引发隐性不一致。
- 想显式指定加载器?用
Class.forName(String, boolean, ClassLoader)重载,第二个参数控制是否初始化类 - 若传入
null作为ClassLoader参数,会退回到启动类加载器(Bootstrap),但多数用户代码无法直接访问它,可能抛ClassNotFoundException - 注意:
Class.forName("X")和X.class行为不同 —— 后者绑定编译期所在类的加载器,前者绑定运行时调用点所在类的加载器
双亲委派被破坏时,同名类冲突风险陡增
Tomcat、Jetty、OSGi 等容器主动打破双亲委派(比如先查 WebApp 自己的 WEB-INF/classes),是为了实现模块隔离,但也让“同名类多份存在”变成常态。这时候,static 字段不再是单例,instanceof 判断可能失效,序列化/反序列化也可能失败。
- 排查技巧:用
jcmd <pid> VM.native_memory summary</pid>或jmap -clstats <pid></pid>查看各加载器已加载的类数量和分布 - 避免硬编码依赖:不要在基础模块里直接 new
com.company.util.ConfigLoader,改用服务接口 + SPI 或依赖注入 - 警惕
Thread.currentThread().setContextClassLoader()被意外覆盖 —— 它常被框架临时切换,但若没恢复,后续反射或资源加载就可能走错加载器
如何安全地跨加载器传递对象
没有银弹,但可收敛风险。核心原则是:尽量不传具体类型,而传接口、字符串、Map、JSON 或原始数据。
- 定义在父加载器(如 shared lib)中的接口,子加载器实现它,可以安全向下转型(因为接口类由父加载器定义)
- 用
ObjectInputStream反序列化前,显式设置resolveClass()回调,确保用目标加载器解析类名 - 避免通过
ClassLoader.getResourceAsStream()加载配置后又用当前类加载器去Class.forName()—— 两者加载器可能不一致,应统一用同一个加载器实例
ClassCastException 或静态状态错乱,调试时看不到栈上明显错误,得靠 getClass().getClassLoader() 打点追踪。











