为什么静态字段初始化失败会导致NoClassDefFoundError错误?

41

这是一个有趣的Java问题。

以下简单的Java程序包含了一个由静态方法初始化的静态字段。实际上,我强制使计算初始化值的方法抛出NullPointException异常,当我访问这样一个静态字段时,会引发NoClassDefFoundError错误。似乎虚拟机将该类视为不完整。

但是当我访问该类时,它仍然可用。

有人知道原因吗?

class TestClass {
    public static TestClass instance = init();

    public static TestClass init() {
       String a = null;
       a.charAt(0); //force a null point exception;
       return new TestClass();
    }
}

class MainClass {
    static public void main(String[] args) {
       accessStatic(); // a ExceptionInInitializerError raised cause by NullPointer
       accessStatic(); //now a NoClassDefFoundError occurs;

       // But the class of TestClass is still available; why?
       System.out.println("TestClass.class=" + TestClass.class);
    }

    static void accessStatic() {
        TestClass a;

        try {
            a = TestClass.instance; 
        } catch(Throwable e) {
            e.printStackTrace();
        }
    }   
}

这对我来说也很惊讶!我联系了一个可能知道答案的朋友,我们会看看他是否回复。 - Ernest Friedman-Hill
我认为是因为类加载器由于静态初始化程序中的异常而无法加载 TestClass - Arun P Johny
@Arun:那部分很简单;我认为他甚至没有询问那个。令人惊讶的是第三个打印输出,它显示MainClass中的TestClass.class实际上持有对物理Class对象的引用。如果类没有正确初始化,为什么我们可以接触它呢? - Ernest Friedman-Hill
1
@Ernest,TestClass.class 似乎不会触发类加载,并且看起来无论类是否已加载都可以访问/可用。 - Bala R
4个回答

39
这类问题的答案通常可以在规范中找到... (§12.4.2) 当类被初始化时会发生什么:
步骤1-4与此问题有些不相关。步骤5是触发异常的原因:
如果Class对象处于错误状态,则无法进行初始化。释放Class对象上的锁并抛出NoClassDefFoundError。
6-8继续初始化,8执行初始值设定项,通常在步骤9中发生:
如果初始值设定项的执行正常完成,则锁定此Class对象,将其标记为完全初始化,通知所有等待线程,释放锁,并正常完成此过程。
但是我们在初始化程序中遇到了错误,因此:
否则,初始化程序必须通过抛出某个异常E而突然完成。如果E的类不是Error或其子类之一,则创建一个ExceptionInInitializerError类的新实例,并使用该对象代替以下步骤中的E。但是,如果由于发生OutOfMemoryError而无法创建ExceptionInInitializerError类的新实例,则在以下步骤中使用OutOfMemoryError对象代替E。
是的,由于空指针异常,我们看到了ExceptionInInitializerError。
11. 锁定类对象,将其标记为错误,通知所有等待的线程,释放锁,并以原因 E 或在前一步骤中确定的其替代项突然完成此过程。(由于一些早期实现中存在缺陷,在类初始化期间发生异常时被忽略,而不是像这里描述的那样引发 ExceptionInInitializerError。) 然后该类被标记为错误,这就是为什么我们第二次从步骤 5 中得到异常的原因。
意外的是第三个打印输出显示,MainClass 中的 TestClass.class 实际上持有一个对物理 Class 对象的引用。 可能是因为 TestClass 仍然存在,只是被标记为错误。它已经被加载和验证过了。

5
好的,这是关于“ NoClassDefFoundError”名称的问题,人们必须阅读规范才能理解它。无论是名称还是Java文档,都没有解释它的含义。因为许多Java大师在多年后甚至仍然不理解它,所以可以说这个名称完全失败了。 - irreputable
1
(由于早期某些实现中存在缺陷,类初始化期间的异常被忽略,而不是像这里描述的那样引发ExceptionInInitializerError。)- 这个缺陷存在于哪些Java版本中?我在Java 1.8.0_25-b17中遇到了这个问题。在类初始化时抛出了NoClassDefFound错误,但原始异常被丢弃了(导致我花费了很多时间来解决我的类路径有什么问题)。 - Alex Spurling
@irreputable,有一个[JDK-6766753](https://bugs.openjdk.java.net/browse/JDK-6766753)的建议是要么引入一个子类,要么改进文档,使得更清楚何时可能会抛出`NoClassDefFoundError`(尽管该报告自2008年以来一直处于打开状态)。 - Marcono1234

19

是的,这通常是引发 NoClassDefFoundError 异常的原因。这只是命名不当。它应该被命名为“类初始化失败异常”之类的名称。

由于名称误导,遇到此错误的 Java 程序员浪费了数百人年的时间试图弄清楚为什么找不到类。

每当出现此异常时,您应该向上检查日志,并尝试找出类在初始化失败时的根本原因。


1
我并不认为它的命名有问题。最多只是在类初始化失败时抛出异常的选择不太好。NoClassDefFoundError 在其他情况下肯定也有其用途(例如,在运行时类路径中找不到类定义,而在编译时类路径中确实可用)。 - BalusC
没有人会因为一个类初始化失败而导致整个网站崩溃。 - irreputable
@StephenC 如果整个应用程序的合理行为取决于对异常情况的非默认处理方式,那么异常系统设计得很糟糕。如果错误应该停止整个应用程序,那么系统默认的未捕获异常处理程序应该具备这种行为。 - Jules
@Jules - 我同意。它应该是系统默认的未捕获异常处理程序的行为...但通常并不是这样。这首先是Java的错误...以及像Tomcat等框架没有补偿它的错误。 - Stephen C
5
到目前为止,“NoClassDefFoundError”的最大问题是它不包含“cause”信息。这可能导致数百万甚至数十亿美元的生产力损失。这是一个极其愚蠢的决定。 - nilskp
显示剩余3条评论

4
当我访问这样一个静态字段时,会引发NoClassDefFoundError错误。似乎虚拟机认为该类不完整。
没错...
但是,当我访问该类时,它仍然可用。
是的。
类加载器尚未尝试删除损坏的类,因为:
- 这将很难做到, - 这将非常难以安全地实现, - 这将使Java虚拟机处于一个状态,在该状态下应用程序可以轻松地浪费大量时间重复加载和重新加载损坏的代码,并且 - 规范说明(或者至少意味着)不应该这样做;有关详细信息,请参见其他答案。
要进入此不一致性可见的状态,您的应用程序必须捕获ClassDefNotFoundError错误(或其超类),并尝试从中恢复。众所周知,Error异常通常无法恢复;也就是说,如果您尝试进行恢复,虚拟机可能会处于不一致的状态。就在这里...相对于正在加载/初始化的类。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接