NoClassDefFoundError 有时对于 System.out.close() 之后的标准 class

NoClassDefFoundError sometimes for a standard class after System.out.close()

我的 java 程序由于 NoClassDefFoundError 而神秘崩溃。神秘之处在于异常消息表明有问题的 class 是 java/util/concurrent/CopyOnWriteArrayList$COWIterator,它是 JRE 的一部分。在其他 JRE classes 没有问题地加载后抛出异常。

异常是从 logback(版本 1.1.3)抛出的,似乎是在它遍历要写入的附加程序列表时(我猜它使用 CopyOnWriteArrayList 来保存列表appenders):

Thread [main] (Suspended (exception NoClassDefFoundError))  
CopyOnWriteArrayList<E>.iterator() line: 959    
AppenderAttachableImpl<E>.appendLoopOnAppenders(E) line: 47 
Logger.appendLoopOnAppenders(ILoggingEvent) line: 273   
Logger.callAppenders(ILoggingEvent) line: 260   
Logger.buildLoggingEventAndAppend(String, Marker, Level, String, Object[], Throwable) line: 442 
Logger.filterAndLog_0_Or3Plus(String, Marker, Level, String, Object[], Throwable) line: 396 
Logger.info(String) line: 600   
MyProgramTextLogger.logStarting() line: 303 
MyProgram.run() line: 1686  
MyProgram.runProgram(MyProgram) line: 790   
MyProgram.main(String[]) line: 712  

当然,logback 代码在创建要迭代的列表时必须已经加载了 CopyOnWriteArrayList class,因此 classloader 必须那时已经正确地完成了它的工作。

更神秘的是,如果我 运行 程序在前台,或者作为由 systemd 控制的守护进程,则不会发生此问题。它仅在我 运行 我的程序作为由 systemd 启动的守护进程的子进程时出现。

实验(通过使用 static 代码块)表明主线程 classloader 可以在程序启动时加载 CopyOnWriteArrayList$COWIterator class OK(程序然后为不同的 JRE class 抛出 NoClassDefFoundError)。就好像 classloader 已经决定失去加载 classes 的能力。

我没有向 java 程序传递任何特殊参数。我为 java 选项使用了完整路径名并提供了 -showversion 选项以确保我每次都使用相同的 JVM 和 JRE。我的命令行很简单:

 /usr/bin/java \
 -showversion \
 -jar /home/myuser/lib/my-app.jar --1 --latest

Java 报告其版本为

 java version "1.7.0_75"
 OpenJDK Runtime Environment (rhel-2.5.4.2.el7_0-x86_64 u75-b13)
 OpenJDK 64-Bit Server VM (build 24.75-b04, mixed mode)

我的程序没有进行任何显式 class 加载,也没有自定义 class 加载程序:它使用默认的 class 加载程序。然而,bkail has pointed out that it should not matter if I did, because the Java platform classes should be loaded by the boot class loader rather than the application class loader.

在我的程序失败的地方,我的代码没有创建任何线程。我的代码不包含任何本机代码,因此没有 "external" 可能混淆事物的线程。但是,我相信 logback 创建了一些线程,当然还有正常的 Java 守护进程线程。

还有一个神秘的方面。在创建 logback 记录器(使用 org.slf4j.LoggerFactory.getLogger(MyProgram.class))和调用导致异常的 Logger.info(String) 方法之间,基本上我的应用程序所做的就是关闭标准输入、标准输出和标准错误输出流,使用对这个方法的调用:

 private static void closeSystemStreams() {
    try {
       System.in.close();
    } catch (IOException e) {
       // Do nothing; should never happen, and there is nothing we can do if
       // it does
    }
    System.out.close();
    System.err.close();
 }

如果我省略关闭代码,程序 运行 就可以了。现在,我可以理解关闭这些流在某些情况下可能会失败(和 some recommend against it),但为什么它会干扰 bootstrap class 加载程序?

我需要做什么才能确保 logback 或 JVM 不会出现此类 class 加载问题?

如果关闭标准流,某些 Java 库代码似乎会变得混乱。也许某处有一个文件描述符值的硬编码检查。这可能是错误,也可能不是错误,具体取决于您对行为的解释程度。不关闭标准流似乎是最安全的。