1. 概述
在大多数情况下,保持应用程序的稳定状态比保持其运行更重要。本文将介绍如何在遇到OutOfMemoryError
时显式停止应用程序。在某些情况下,如果没有正确的处理,应用程序可能会进入不正确的状态。
2. OutOfMemoryError
OutOfMemoryError
是应用程序外部的一种异常,通常无法恢复,至少在大多数情况下是这样。错误名称暗示应用程序没有足够的RAM,但这并不完全正确。更准确地说,应用程序无法分配请求的内存量。
在一个单线程应用中,情况相对简单。如果我们遵循指南,并且不捕获OutOfMemoryError
,应用程序会终止,这是处理这种错误的预期方式。
在特定情况下,捕获OutOfMemoryError
可能是合理的,也有一些更具体的情况,其中在发生后继续执行可能是合理的。然而,在大多数情况下,OutOfMemoryError
意味着应该停止应用程序。
3. 多线程
多线程是现代应用的核心部分。线程对于异常遵循一种拉斯维加斯式的规则:线程内部的问题留在线程内部。这并不总是成立,但我们可以认为这是一种普遍行为。
因此,即使线程中出现最严重的错误,除非我们明确处理,否则不会传播到主应用程序。让我们看一个内存泄漏的例子:
public static final Runnable MEMORY_LEAK = () -> {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(tenMegabytes());
}
};
private static byte[] tenMegabytes() {
return new byte[1024 * 1014 * 10];
}
如果我们在单独的线程中运行这段代码,应用程序不会失败:
@Test
void givenMemoryLeakCode_whenRunInsideThread_thenMainAppDoestFail() throws InterruptedException {
Thread memoryLeakThread = new Thread(MEMORY_LEAK);
memoryLeakThread.start();
memoryLeakThread.join();
}
这是因为导致OutOfMemoryError
的所有数据都与线程相关联。当线程死亡时,List
失去了垃圾回收根,可以被收集。因此,最初导致OutOfMemoryError
的数据随着线程的死亡而被移除。
如果我们多次运行此代码,应用程序不会失败:
@Test
void givenMemoryLeakCode_whenRunSeveralTimesInsideThread_thenMainAppDoestFail() throws InterruptedException {
for (int i = 0; i < 5; i++) {
Thread memoryLeakThread = new Thread(MEMORY_LEAK);
memoryLeakThread.start();
memoryLeakThread.join();
}
}
同时,垃圾收集日志显示如下情况:
每次循环中,我们将消耗6GB的可用RAM,杀死线程,运行垃圾收集,删除数据,然后继续。我们得到了一个堆的过山车,没有任何合理的工作,但应用程序不会失败。
同时,我们可以在日志中看到错误。在某些情况下,忽略OutOfMemoryError
是有道理的,因为我们不想因为bug或用户攻击而关闭整个web服务器。
此外,实际应用中的行为可能会有所不同。线程之间可能存在交互和额外共享资源。因此,任何线程都可能抛出OutOfMemoryError
。这是一个异步异常,它们不受特定行的约束。然而,只要OutOfMemoryError
没有发生在主线程中,应用程序仍然会运行。
4. 杀死JVM
在某些应用中,线程产生关键工作并需要可靠地完成。最好停止所有操作,查找并解决问题。
想象一下,我们正在处理一个包含历史银行业务数据的巨大XML文件。我们将数据块加载到内存中,进行计算,然后将结果写入磁盘。这个例子可以更复杂,但主要思想是有时,我们非常依赖线程中过程的事务性和正确性。
幸运的是,JVM将OutOfMemoryError
视为特殊情况,我们可以通过以下参数在应用程序中使用它来退出或在遇到OutOfMemoryError
时崩溃JVM:
-XX:+ExitOnOutOfMemoryError
-XX:+CrashOnOutOfMemoryError
如果运行我们的示例之一,应用程序将停止。这将使我们能够调查问题并查看发生了什么。
这些选项之间的区别在于-XX:+CrashOnOutOfMemoryError
会产生崩溃转储:
#
# A fatal error has been detected by the Java Runtime Environment:
#
# Internal Error (debug.cpp:368), pid=69477, tid=39939
# fatal error: OutOfMemory encountered: Java heap space
#
...
它包含可用于分析的信息。为了简化这个过程,我们还可以在OutOfMemoryError
时自动创建堆转储进行进一步调查。有一个特殊的选项可以做到这一点。
对于多线程应用,我们也可以生成线程转储。它没有专用的参数,但我们可以使用脚本并在OutOfMemoryError
时触发它。
如果我们想以类似的方式处理其他异常,我们必须使用Futures
来确保线程按预期完成工作。将异常包装成OutOfMemoryError
以避免实现正确的线程间通信是个糟糕的主意:
@Test
void givenBadExample_whenUseItInProductionCode_thenQuestionedByEmployerAndProbablyFired()
throws InterruptedException {
Thread npeThread = new Thread(() -> {
String nullString = null;
try {
nullString.isEmpty();
} catch (NullPointerException e) {
throw new OutOfMemoryError(e.getMessage());
}
});
npeThread.start();
npeThread.join();
}
5. 总结
在这篇文章中,我们讨论了OutOfMemoryError
经常会使应用程序处于不正确状态。尽管在某些情况下我们可以恢复,但从整体上来说,我们应该考虑停止并重新启动应用程序。
虽然单线程应用程序不需要对OutOfMemoryError
进行额外处理,多线程代码需要额外的分析和配置,以确保应用程序能够退出或崩溃。
如往常一样,所有的代码都可以在GitHub上找到。