如果没有未使用的类,JVM会抛出吗



考虑程序:

public class Test {
public static void main(String[] args) {
if (Arrays.asList(args).contains("--withFoo")) {
use(new Foo());
}
}
static void use(Foo foo) {
// do something with foo
}
}

如果程序是在没有参数的情况下启动的,那么运行时类路径中是否需要Foo?

研究

当报告链接错误时,Java语言规范相当模糊:

该规范允许在链接活动(以及由于递归,加载)发生时实现灵活性,前提是尊重Java编程语言的语义,类或接口在初始化之前经过完全验证和准备,并且在链接期间检测到的错误被抛出到程序中的某个点,在该点处程序采取了可能需要链接到错误中涉及的类或接口的一些动作。

我的测试表明只有当我实际使用Foo:时才会抛出LinkageErrors

$ rm Foo.class
$ java Test
$ java Test --withFoo
Exception in thread "main" java.lang.NoClassDefFoundError: Foo
at Test.main(Test.java:11)
Caused by: java.lang.ClassNotFoundException: Foo
at java.net.URLClassLoader$1.run(Unknown Source)
at java.net.URLClassLoader$1.run(Unknown Source)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 1 more

这种行为可以信赖吗?或者有没有主流JVM链接未使用的代码?如果是,我如何隔离未使用的代码,使其仅在需要时链接?

您只需要对测试代码进行一些小的更改就可以回答这个问题。

将类型层次结构更改为

class Bar {}
class Foo extends Bar {}

和的程序

public class Test {
public static void main(String[] args) {
if (Arrays.asList(args).contains("--withFoo")) {
use(new Foo());
}
}
static void use(Bar foo) {
// don't need actual code
}
}

现在,如果Foo不存在,即使在进入main方法(使用HotSpot)之前,程序也会失败并出现错误。原因是验证器需要Foo的定义来检查将其传递给期望Bar的方法是否有效。

如果类型完全匹配,或者如果目标类型是java.lang.Object(其中赋值始终有效),HotSpot会采取捷径,不加载类型。这就是为什么当Foo不存在时,原始代码不会提前抛出的原因。

底线是,抛出错误的确切时间点取决于实现,例如,可能取决于实际的验证器实现。正如您已经提到的,所有可以保证的是,尝试执行需要链接的操作将抛出以前检测到的链接错误。但很有可能你的程序从来没有尝试过。

我想像这样的东西是未定义的(有点像,请参阅底部)。我们知道它是如何为oracle虚拟机工作的,但它是虚拟机的实现细节。虚拟机也可以选择立即加载所有类。

你可以在VM规范(强调矿)中找到:

链接一个类或接口包括验证和准备该类或接口、其直接超类、其直接超级接口以及其元素类型(如果是数组类型)(如有必要)解析类或接口中的符号引用是链接的可选部分

该规范允许实现在链接活动(以及由于递归的原因,加载)何时发生方面的灵活性。。。

再往下看:

Java虚拟机指令anewarraycheckcastgetfieldgetstaticinstanceofinvokedynamicinvokeinterfaceinvokespecialinvokestaticinvokevirtualldcldc_wmultianewarraynewputfieldputstatic对运行时常量池进行符号引用。执行这些指令中的任何一个都需要解析其符号引用。

Resolution是从运行时常量池中的符号引用动态确定具体值的过程。

use(new Foo());编译为:

14: new           #5                  // class Foo
17: dup
18: invokespecial #6                  // Method Foo."<init>":()V
21: invokestatic  #7                  // Method use:(LFoo;)V

因此,这些需要Foo的分辨率,但程序中的其他内容都不需要。


然而,它也指出(附在一个例子中,这就是我一开始错过它的原因):

无论采用哪种策略,在解析过程中检测到的任何错误都必须在程序中(直接或间接)使用对类或接口的符号引用的点上抛出。

因此,虽然在加载Test类时可能会发现具有解析能力的错误,但只有在实际使用错误的符号引用时才会抛出该错误。

我不得不说,在您的情况下,我非常想使用反射来创建一个始终存在的接口,以完全绕过问题。大致如下:

// This may or may not be present
package path.to.foo;
public class Foo implements IFoo {
public void doFooStuff() {
...
}
}
// This is always present
package path.to.my.code;
public interface IFoo {
public void doFooStuff();
}
// Foo may or may not be present at runtime, but this always compiles
package path.to.my.code;
public class Test {
public static void main(String[] args) {
if (Arrays.asList(args).contains("--withFoo")) {
Class<IFoo> fc = Class.forName("path.to.foo.Foo");
IFoo foo = (IFoo)fc.newInstance();
use(foo);
}
}
static void use(IFoo foo) {
// do something with foo
}
}

[编辑]我知道这并不能直接回答问题,但这似乎是一个比你旅行的地方更好的解决方案。

最新更新