如果未使用的 class 不存在,JVM 会抛出异常吗?

Does the JVM throw if an unused class is absent?

考虑程序:

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 语言规范相当模糊:

This specification allows an implementation flexibility as to when linking activities (and, because of recursion, loading) take place, provided that the semantics of the Java programming language are respected, that a class or interface is completely verified and prepared before it is initialized, and that errors detected during linkage are thrown at a point in the program where some action is taken by the program that might require linkage to the class or interface involved in the error.

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

$ 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.run(Unknown Source)
        at java.net.URLClassLoader.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?如果是这样,我如何隔离未使用的代码,以便仅在需要时链接它?

我想这样的东西是未定义的(有点,见底部)。我们知道 oracle VM 是如何工作的,但它是 VM 的实现细节。 VM 也可以选择立即加载所有 classes。

您可以在 VM spec(强调我的)中找到:

Linking a class or interface involves verifying and preparing that class or interface, its direct superclass, its direct superinterfaces, and its element type (if it is an array type), if necessary. Resolution of symbolic references in the class or interface is an optional part of linking.

This specification allows an implementation flexibility as to when linking activities (and, because of recursion, loading) take place...

再往下:

The Java Virtual Machine instructions anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokevirtual, ldc, ldc_w, multianewarray, new, putfield, and putstatic make symbolic references to the run-time constant pool. Execution of any of these instructions requires resolution of its symbolic reference.

Resolution is the process of dynamically determining concrete values from symbolic references in the run-time constant pool.

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 的分辨率,但程序中的其他任何东西都不需要。


但是,它还指出(附加到示例中,这就是我一开始错过它的原因):

Whichever strategy is followed, any error detected during resolution must be thrown at a point in the program that (directly or indirectly) uses a symbolic reference to the class or interface.

所以虽然在加载Test class时可能会发现错误,但只有在实际使用错误的符号引用时才会抛出错误。

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

// 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
    }
}

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

您只需对测试代码进行少量更改即可回答该问题。

将类型层次结构更改为

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 的方法是否有效。

HotSpot 采用 short-cut,不加载类型,如果类型完全匹配或目标类型为 java.lang.Object,则分配始终有效。这就是为什么当 Foo 不存在时您的原始代码不会提前抛出。

最重要的是,抛出错误的确切时间点取决于实现,例如可能取决于实际的验证器实现。正如您已经引用的那样,可以保证的是,尝试执行需要链接的操作将抛出先前检测到的链接错误。但是您的程序完全有可能永远无法进行尝试。