我看到一些类加载行为似乎与JVM规范不一致,我想知道这是否是一个bug。如果没有,希望有人能解释为什么。
下面的示例代码只是从它的主方法打印hello。它有一个未使用的方法,该方法包含对一个方法的方法调用,该方法声明它接受'C'(这是一个接口)作为参数。
当main被执行(类路径中没有A, B和C)时,会抛出接口C的ClassNotFound错误(注意C在运行时实际上不需要,因为它只在从未执行的方法中被引用)
这似乎违反了JVM规范
Java VM Spec第2版第2.17.1节说:
关于何时执行解析的唯一要求是在解析期间检测到的任何错误必须在程序中某个点抛出,该点由程序采取的某些操作可能直接或间接地需要链接到与错误有关的类或接口
Java VM Spec第2版第2.17.3节说:
Java编程语言允许在链接活动(以及,由于递归,加载)发生时实现灵活性,只要语言的语义得到尊重,类或接口在初始化之前得到完全验证和准备。并且在链接期间检测到的错误在程序中某个点被抛出,此时程序可能需要链接到错误所涉及的类或接口。
注意:如果我在定义中将形参的类型更改为类而不是接口,则代码将加载并正确执行。
/**
* This version fails, the method call in neverCalled() is to a method whose
* parameter definition is for an Interface
*/
public class Main {
public void neverCalled(){
A a = new A();
B b = new B(); // B implements C
//method takeInter is declared to take paramters of type Interface C
//This code is causes a ClassNotFound error do be thrown when Main
//is loaded if A, B, and C is not in the class path
a.takeInter(b);
}
public static void main(String[] args) {
System.out.println("Hello...");
}
}
/**
* This version runs, the method call in neverCalled() is to a method whose
* parameter definition is for a Class
*/
public class Main {
public void neverCalled(){
A a = new A();
B b = new B(); // B implements C
//method takeInter is declared to take paramters of type Interface C
//This code is causes a ClassNotFound error do be thrown when Main
//is loaded if A, B, and C is not in the class path
a.takeClass(b);
}
public static void main(String[] args) {
System.out.println("Hello...");
}
}
public class A {
public void takeClass(B in){};
public void takeInter(C in){}
}
public class B implements C {}
public interface C {}
,
我并不是有意断章取义,我只是把我认为相关的部分摘掉了。谢谢你帮我理解这个问题。
不管怎样,说明书对我来说似乎很清楚。它说错误必须在一个不是的点被点抛出。当然,我是在阅读了《Java虚拟机内幕》第8章之后才阅读了虚拟机规范,所以可能这影响了我的解释。
从http://www.artima.com/insidejvm/ed2/linkmod.html如第7章"类的生命周期"所述,允许Java虚拟机的不同实现在程序执行期间的不同时间执行解析。实现可以选择通过以下方式链接所有内容:首先遵循初始类的所有符号引用,然后遵循后续类的所有符号引用,直到每个符号引用都被解析。在这种情况下,应用程序将在调用main()方法之前被完全链接。这种方法被称为早期解决方案。或者,实现可以选择等到最后一刻才解析每个符号引用。在这种情况下,Java虚拟机只有在运行的程序第一次使用符号引用时才会解析它。这种方法被称为后期解决。实现也可以在这两个极端之间使用解析策略。
尽管Java虚拟机实现在选择何时解析符号引用方面有一定的自由,但是每个Java虚拟机都必须给人一种使用晚解析的印象。无论特定的Java虚拟机何时执行其解析,它总是会抛出任何错误,这些错误是由于在程序执行过程中第一次实际使用符号引用时试图解析符号引用而导致的。这样,对用户来说,它总是显得好像分辨率晚了。如果Java虚拟机进行早期解析,并且在早期解析过程中发现类文件丢失,则它不会通过抛出适当的错误来报告丢失的类文件,直到稍后在程序中实际使用该类文件中的某些内容时。如果程序从不使用该类,则永远不会抛出错误。
这里是一个更简单的例子,但也失败了。
public class Main {
public void neverCalled() {
A a = new A();
B b = new B();
a.takeInter(b);
}
public static void main(String[] args) {
System.out.println("Hello...");
}
}
class A {
public void takeInter(A in) {
}
}
class B extends A {
}
class C {
}
字节码
中的public void neverCalled();
Code:
0: new #2 // class A
3: dup
4: invokespecial #3 // Method A."<init>":()V
7: astore_1
8: new #4 // class B
11: dup
12: invokespecial #5 // Method B."<init>":()V
15: astore_2
16: aload_1
17: aload_2
18: invokevirtual #6 // Method A.takeInter:(LA;)V
21: return
b
隐式地转换为A
,它似乎需要检查这个
如果您关闭所有验证,则不会发生错误。
$ rm A.class B.class C.class
$ java -Xverify:none -cp . Main
Hello...
$ java -cp . Main
Exception in thread "main" java.lang.NoClassDefFoundError: A
你在第2.17.1节引用的大量的断章取义。下面用粗体表示。当在上下文中阅读时,很明显"errors…"必须在程序的某个点抛出…"表示"errors…"必须在程序到达点之前抛出…"。这句话本身可以写得更好——但它是而不是。
初始联动时,解析步骤可选。一个实现可以解析来自类或类的符号引用接口很早就被连接起来了,甚至到了解析类和接口中的所有符号引用被递归地进一步引用。(这个决议可能会导致由于进一步加载和链接步骤而导致的错误。)这个实现选择代表一种极端,类似于静态的简单实现中已经进行了多年的链接C语言
实现可以选择解析符号引用只有在实际使用的时候;对所有人持续使用这一战略符号引用代表了"最懒"的解析形式。在这种情况下,如果终结者有几个对另一个的符号引用类时,引用可能一次解析一个,也可能不解析的执行过程中从未使用过这些引用程序。
关于何时执行解析的唯一要求是方法中的某个点抛出在解析过程中检测到的任何错误程序,其中一些操作由程序直接执行或者间接地,要求链接到所涉及的类或接口这个错误。 在"静态"示例中描述了实现选择以前,加载和链接错误可能会在程序运行之前发生如果它们涉及类或类中提到的接口,则执行或任何进一步递归引用的类和接口。在一个执行"最懒"决议的系统中,这些错误只会在使用符号引用时抛出。
后面的两个句子使意思很清楚。