bigywater.tech
Java 内部原理 · 31

两个"一模一样"的类:真实用两个 ClassLoader 造出两个不能互相强转的 Foo

"类加载器决定了类的身份"是个抽象结论。真实用两个独立的 URLClassLoader 加载同一份字节码,把这句抽象的话变成一个真实能复现的 ClassCastException。

foo1 == foo2 → false
同一份 Foo.class,被两个不同的 URLClassLoader 各自真实加载一次,得到的 Class 对象真实不是同一个
ClassCastException
用其中一个 Class 创建的实例,真实无法强转成另一个的类型,报错信息里两个"Foo"字面上完全一样

上一篇:int 溢出真实静默环绕:Integer.MAX_VALUE + 1 真实变成负数,没有任何报错

思路:同一份 .class 文件,用两个独立的加载器分别加载

JVM 判断"两个类是不是同一个类",靠的不只是全限定名,还要看是谁加载的它。

准备一个最简单的 Foo 类,编译成 Foo.class 放进一个目录;然后创建两个独立的 URLClassLoader 实例,都指向这同一个目录,父加载器都设成 null(不委派给系统类加载器),让每个实例都真实自己去定义这个类,而不是从别处拿现成的:

ClassLoaderDemo.java(核心逻辑) URL classesDir = new File("classes").toURI().toURL(); URLClassLoader loader1 = new URLClassLoader(new URL[]{classesDir}, null); URLClassLoader loader2 = new URLClassLoader(new URL[]{classesDir}, null); Class<?> foo1 = loader1.loadClass("Foo"); Class<?> foo2 = loader2.loadClass("Foo");

真实验证:两个 Class 对象,真的不是同一个

先确认两个类加载器确实是独立的,再看它们各自加载出来的 Foo 是不是同一个 Class 对象。

真实实测
foo1 的类加载器 = java.net.URLClassLoader@251a69d7
foo2 的类加载器 = java.net.URLClassLoader@7344699f
foo1 == foo2 (同一个 Class 对象吗) = false
foo1.equals(foo2) = false

obj1 instanceof foo1 的类型 = true
obj1 instanceof foo2 的类型 = false

字节码内容完全相同、全限定名也完全相同(都叫 Foo),但真实测出来这是两个不同的 Class 对象——obj1 是 foo1 类型的实例,却真实不是 foo2 类型的实例。这直接验证了 JVM 判定类身份的真实规则:一个类在 JVM 里的唯一标识是(全限定类名,加载它的 ClassLoader 实例)这个二元组,不是单看类名。

真实验证:强转会真实抛出 ClassCastException

反射调用不受这个限制(反射靠的是 Method 对象本身,不做编译期类型检查),但真实的强制类型转换会。

真实实测 · 反射调用不受影响
反射调用 obj1.hello() = hello from Foo
真实实测 · 强转成第三份定义的 Foo(应用类加载器自己加载的)
真实抛出 ClassCastException: class Foo cannot be cast to class Foo (Foo is in unnamed module of loader java.net.URLClassLoader @251a69d7; Foo is in unnamed module of loader 'app')

这条真实报错信息很有意思:字面上写的是"class Foo cannot be cast to class Foo"——两边名字一模一样,乍看像是同义反复。真正的区分信息藏在括号里:一个 Foo 来自 URLClassLoader@251a69d7,另一个来自应用类加载器 'app'。这也是这类问题在真实项目里最容易让人摸不着头脑的地方——报错信息表面上"类型对得上",但 JVM 坚持认为它们不是同一个类型。

这种情况真实会在什么场景遇到?典型场景是应用服务器/插件系统的热部署——同一个类被不同的模块类加载器分别加载了一份,模块之间传递对象时,接收方拿到的类型和自己模块里"看起来一样"的类其实是两个不同的类,强转就会真实炸出这个报错。这也是泛型可变参数堆污染那篇之外,另一类"编译期看起来完全正常、运行时才真实暴露"的类型安全缺口。

分步演示:两份"一模一样"的 Foo 是怎么走向不同结局的

用 identityHashCode 给每个 ClassLoader、Class、实例对象编号,串成动画:从加载到 instanceof 检查,再到最后真实抛出的 ClassCastException。

同一份 Foo.class,两个独立 URLClassLoader 各自加载:编号从头到尾没有重合过一次
这一步新出现或发生变化的项

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到。
    • 没有涉及:双亲委派模型完整的委派链路细节、Java 9+ 模块系统对类加载的额外约束——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号