为什么包含方法的类在未使用的代码中调用缺少的接口会导致Java类加载错误?

时间:2011-11-18 16:22:46

标签: java classloader

我看到一些类加载行为似乎与JVM规范不一致,我想知道这是不是一个bug。或者如果没有,希望有人可以解释原因。

下面找到的示例代码只是从其main方法打印hello。它有一个未使用的方法,它包含对方法的方法调用,该方法声明它将'C'(作为接口)作为参数。

当main执行(类路径中没有A,B和C)时,接口C会引发ClassNotFound错误。(因为它只是在运行时才需要注意C)在从不执行的方法中引用)。

这似乎违反了JVM规范

Java VM Spec第2.17.1节第2版说:

  

关于何时执行解析的唯一要求是解析期间检测到的任何错误必须抛出程序中的某个点,程序可能会直接或间接地需要链接到该类或错误涉及的界面

Java VM Spec的第2.17.3节,第2版说:

  

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章中的以下内容后阅读了VM规范,所以也许这对我的解释有所了解。

来自,http://www.artima.com/insidejvm/ed2/linkmod.html

  

如第7章“类的生命周期”所述,允许Java虚拟机的不同实现在程序执行期间的不同时间执行解析。实现可以选择通过遵循来自初始类的所有符号引用,然后跟随后续类中的所有符号引用来预先链接所有内容,直到每个符号引用都已被解析。在这种情况下,应用程序将在调用main()方法之前完全链接。这种方法称为早期解决方案。或者,实现可以选择等到最后一分钟来解析每个符号引用。在这种情况下,Java虚拟机仅在运行程序首次使用时才会解析符号引用。这种方法称为延迟解决方案。实现也可以在这两个极端之间使用解决策略。

     

尽管Java虚拟机实现在选择何时解析符号引用方面有一定的自由度,但每个Java虚拟机都必须给人以外观的印象:它使用后期解析。无论何时特定的Java虚拟机执行其解析,它总是会抛出任何错误,这些错误是在程序执行时尝试解析符号引用时产生的,这是第一次实际使用符号引用即可。通过这种方式,用户将始终看起来好像分辨率很晚。如果Java虚拟机提前解析,并且在早期解析期间发现缺少类文件,则在实际使用该类文件中的某些内容时,它将不会通过抛出相应的错误来报告该类文件。如果程序从不使用该类,则永远不会抛出该错误。

2 个答案:

答案 0 :(得分:6)

这是一个更简单的例子,也失败了。

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

答案 1 :(得分:5)

来自Section 2.17.1的引文 大量 脱离上下文。它在下面以粗体显示。在上下文中读取时,很明显“错误......必须抛出程序中的某个点...... ”表示“错误...必须在程序启动时抛出达到一个点...... “。这个句子本身可以说得更好 - 但它本身并不是

  

初始链接时,解决步骤是可选的。一个   实现可以解决来自类的符号引用   很早就联系起来的界面,甚至到了   从类和接口解析所有符号引用   进一步以递归方式引用。 (此决议可能导致   进一步加载和链接步骤的错误。)这个实现   选择代表一个极端,类似于静态的那种   多年来在简单实现中已经完成的联系   C语言。

     

实现可以改为选择解析符号引用   只有在实际使用时;所有人都一致地使用这种策略   符号引用将代表“最懒惰”的解决方式。   在这种情况下,如果Terminator有几个符号引用另一个   class,引用可能一次解决一个,也可能不解决   完全没有,如果在执行过程中从未使用过这些引用   程序

     

关于何时执行解析的唯一要求是   在解决过程中检测到的任何错误都必须抛出   程序,程序可能直接采取某些行动   或间接地,需要与所涉及的类或接口的链接   错误。 在描述的“静态”示例实现选项中   之前,加载和链接错误可能在程序之前发生   如果它们涉及类中提到的类或接口,则执行   终结符或任何其他递归引用的类和   接口。在实现“最懒惰”分辨率的系统中,   只有在使用符号引用时才会抛出这些错误。

后两句话的含义非常明确。