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 正在那里被用完:ParentClassFromLibOne
和 ChildClassFromLibTwo
。后者从前者延伸而来。
涉及两个外部库:
- 一个库称为
libone
并包含 ParentClassFromLibOne
class。应用程序在 class 路径中包含此库,用于编译和执行。
- 第二个库称为
libtwo
,包含 ChildClassFromLibTwo
class。应用程序在 class 编译路径中包含此库, 但不用于执行 。
据我了解,Java 运行时应尝试加载 ChildClassFromLibTwo
(在 class 路径中 而不是 在运行时)在这一行:
final ChildClassFromLibTwo obj = new ChildClassFromLibTwo();
鉴于此 class 不在 class 路径中,应抛出 ClassNotFoundException
,并且鉴于此行位于 try...catch (Throwable)
内,System.out.println
无论如何都应该执行末尾的行。
但是,我得到的是 ClassNotFoundException
在 DynamicClassLoadingAppKO
本身被加载时抛出,显然 在 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
我需要帮助来理解为什么这会发生在我身上:
使用 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 正在那里被用完:ParentClassFromLibOne
和 ChildClassFromLibTwo
。后者从前者延伸而来。
涉及两个外部库:
- 一个库称为
libone
并包含ParentClassFromLibOne
class。应用程序在 class 路径中包含此库,用于编译和执行。 - 第二个库称为
libtwo
,包含ChildClassFromLibTwo
class。应用程序在 class 编译路径中包含此库, 但不用于执行 。
据我了解,Java 运行时应尝试加载 ChildClassFromLibTwo
(在 class 路径中 而不是 在运行时)在这一行:
final ChildClassFromLibTwo obj = new ChildClassFromLibTwo();
鉴于此 class 不在 class 路径中,应抛出 ClassNotFoundException
,并且鉴于此行位于 try...catch (Throwable)
内,System.out.println
无论如何都应该执行末尾的行。
但是,我得到的是 ClassNotFoundException
在 DynamicClassLoadingAppKO
本身被加载时抛出,显然 在 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
).
[1] https://github.com/spring-projects/spring-boot/issues/8181