JVM class 加载中的异常行为(确实需要 class 之前的 ClassNotFoundException)

Unexpected behaviour in JVM class loading (ClassNotFoundException before the class is really needed)

我需要帮助来理解为什么这会发生在我身上:

使用 Java 1.8.0_131,我有一个 class 这样的:

public class DynamicClassLoadingAppKO {

    /*
     * THIS VERSION DOES NOT WORK, A ClassNotFoundException IS THROWN BEFORE EVEN EXECUTING main()
     */


    // If this method received ChildClassFromLibTwo, everything would work OK!
    private static void showMessage(final ParentClassFromLibOne obj) {
        System.out.println(obj.message());
    }


    public static void main(final String[] args) throws Throwable {

        try {

            final ChildClassFromLibTwo obj = new ChildClassFromLibTwo();
            showMessage(obj);

        } catch (final Throwable ignored) {
            // ignored, we just wanted to use it if it was present
        }

        System.out.println("This should be displayed, but no :(");

    }

}

另外两个 classes 正在那里被用完:ParentClassFromLibOneChildClassFromLibTwo。后者从前者延伸而来。

涉及两个外部库:

据我了解,Java 运行时应尝试加载 ChildClassFromLibTwo(在 class 路径中 而不是 在运行时)在这一行:

final ChildClassFromLibTwo obj = new ChildClassFromLibTwo();

鉴于此 class 不在 class 路径中,应抛出 ClassNotFoundException,并且鉴于此行位于 try...catch (Throwable) 内,System.out.println 无论如何都应该执行末尾的行。

但是,我得到的是 ClassNotFoundExceptionDynamicClassLoadingAppKO 本身被加载时抛出,显然 main() 方法执行之前 ,因此不会被 try...catch.

捕获

对我来说更奇怪的是,如果我更改 showMessage() 方法的签名,而不是接收 的参数,那么这种行为就会消失并且一切都按我预期的那样工作parentclass,直接就是childclass:

/*
 * THIS VERSION WORKS OK, BECAUSE showMessage RECEIVES THE CHILD CLASS AS A PARAMETER
 */
private static void showMessage(final ChildClassFromLibTwo obj) {
    System.out.println(obj.message());
}

这怎么可能?我在 class 加载过程中遗漏了什么?

为了测试方便,我创建了一个 GitHub 存储库来复制此行为 [1]。

[1] https://github.com/danielfernandez/test-dynamic-class-loading/tree/20170504

这比您想象的要复杂一些。当加载 class 时,将验证所有功能。在验证阶段也会加载所有引用的 classes,因为需要它们来计算字节码中任何给定位置堆栈上的确切类型。

如果你想要那种懒惰的行为,你必须将 -noverify 选项传递给 Java,这将延迟 classes 的加载,直到第一次执行引用它们的函数.但是,当您无法完全控制将加载到 JVM 中的 class 时,出于安全原因不要使用 -noverify

好的,这个 Spring 引导票 [1] 中解释了为什么会发生这种情况的详细信息,我很幸运能够被 Andy Wilkinson 及时指出。在我看来,这绝对是一个困难的过程。

显然,在这种情况下发生的情况是,当调用 class 本身被加载时,验证器启动并看到 showMessage() 方法接收类型为 ParentClassFromLibOne 的参数.到目前为止一切顺利,即使 ParentClassFromLibOne 在运行时不在 class 路径中,也不会在这个阶段引发 ClassNotFoundException

但显然验证者 也扫描方法代码 并注意到 main() 中有一个对 showMessage() 方法的调用。不作为参数传递的调用 ParentClassFromLibOne,而是不同 class 的对象:ChildClassFromLibTwo.

所以在这种情况下,验证者确实会尝试加载 ChildClassFromLibTwo,以便能够检查它是否真的从 ParentClassFromLibOne.

扩展

有趣的是如果 ParentClassFromLibOne 是一个接口,就不会发生这种情况,因为接口被视为 Object 进行分配。

此外,如果 showMessage(...) 直接要求 ChildClassFromLibTwo 作为参数,则不会发生这种情况,因为在这种情况下,验证者不需要加载子 class 来检查它与...兼容。

Daniel,我赞成你的回答,但我不会将其标记为已接受,因为我认为它无法解释在验证时发生这种情况的真正原因(导致 ClassNotFoundException).

的不是方法签名中的 class

[1] https://github.com/spring-projects/spring-boot/issues/8181