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();
    }
}

同时,垃圾收集日志显示如下情况:

OracleVGCLabelsusedheapafter_6

每次循环中,我们将消耗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上找到。


原始标题:Shutting Down on OutOfMemoryError in Java | Baeldung