
java 类加载器遵循双亲委派模型,但自定义 classloader(如 urlclassloader)可绕过该机制,导致同一类被多次加载为不同 class 对象;判断类是否“相同”,需同时比较类名与所属 classloader。
在 Java 中,“同一个类”并非仅由全限定类名(如 example.ToBeLoaded)决定,而是由 类名 + 加载它的 ClassLoader 实例 共同唯一确定。这意味着:即使两个 Class 对象表示完全相同的字节码、来自同一 JAR 文件、拥有相同包名和类名,只要它们由不同的 ClassLoader 实例加载,JVM 就会将它们视为完全独立、互不兼容的类型。
这正是你示例中 child2 == child1 和 child2.equals(child1) 均返回 false 的根本原因:
ClassLoader cl1 = new URLClassLoader(new URL[]{jarUrl}, Main.class.getClassLoader());
ClassLoader cl2 = new URLClassLoader(new URL[]{jarUrl}, Main.class.getClassLoader());
Class> child1 = cl1.loadClass("example.ToBeLoaded");
Class> child2 = cl2.loadClass("example.ToBeLoaded");
System.out.println(child2 == child1); // false —— 不是同一个 Class 对象
System.out.println(child2.equals(child1)); // false —— JVM 认为它们是不同类虽然 cl1 和 cl2 都以 Main.class.getClassLoader()(即 AppClassLoader)为父加载器,但双亲委派仅在 loadClass() 方法内部触发查找逻辑:当 cl1.loadClass(...) 被调用时,它会先委托父加载器尝试加载;若父加载器未找到(例如 example.ToBeLoaded 不在 classpath 中、未被 AppClassLoader 加载过),则 cl1 会自行从指定 JAR 中加载并定义该类。同理,cl2 也会走一遍相同流程——由于父加载器未命中,两次加载均发生在子加载器层面,最终生成两个隔离的 Class 实例。
⚠️ 关键注意点:
立即学习“Java免费学习笔记(深入)”;
- 双亲委派 ≠ 强制共享:它保证“优先复用父加载器已加载的类”,但不保证父加载器一定会加载某个类。若父加载器无法加载(如类不在其路径下),子加载器仍会自行加载,从而产生多个副本。
- 类型隔离性:cl1.loadClass("X") 和 cl2.loadClass("X") 加载的类不可互相赋值或强制转换,否则抛出 ClassCastException;
- 内存中真实存在多个 Class 对象,各自拥有独立的静态变量、方法区元数据等;
- Class::getName()、Class::getCanonicalName() 等方法返回值相同,但 Class::getClassLoader() 返回不同实例。
这种机制并非缺陷,而是企业级应用(如 Tomcat、WebLogic)实现应用隔离的核心基础:多个 Web 应用可各自部署不同版本的 commons-lang 或 Jackson,通过独立的 WebAppClassLoader 加载,避免版本冲突。此时,“相同类名 + 不同加载器 = 不同类型” 是刻意设计的沙箱行为。
✅ 正确判断两个 Class 是否“真正相同”的方式是:
boolean sameClass = clazz1 == clazz2; // 必须是同一个 Class 对象引用 // 或更严谨地(含 null 安全): boolean sameClass = Objects.equals(clazz1, clazz2); // Class.equals() 已重写为引用比较
切勿依赖类名字符串相等,也不要试图通过字节码比对来判定——JVM 不提供运行时语义等价性检查,且类加载过程本身可能包含字节码增强(如 Lombok、AspectJ),进一步加剧差异。
总结:Java 的类加载模型以 ClassLoader 为命名空间边界。理解“类的同一性由加载器共同定义”,是掌握模块化、热部署、OSGi、Spring Boot DevTools 等高级特性的前提。










