Java中发生OutOfMemoryError时关闭

2025/04/26

1. 概述

维护应用程序的一致状态比保持其运行更重要,在大多数情况下都是如此。

在本教程中,我们将学习如何在发生OutOfMemoryError时明确停止应用程序。在某些情况下,如果没有正确的处理,我们可能会在错误的状态下继续运行应用程序。

2. OutOfMemoryError

OutOfMemoryError错误发生在应用程序外部,并且至少在大多数情况下是不可恢复的。该错误的名称暗示应用程序没有足够的RAM,但这并非完全正确。更准确地说,应用程序无法分配所请求的内存量

在单线程应用程序中,情况非常简单,如果我们遵循指南并且没有捕获OutOfMemoryError,应用程序就会终止,这是处理此错误的预期方式。

在某些特定情况下,捕获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触发它。

如果我们想以类似的方式处理其他异常,则必须使用Future来确保线程按预期完成其工作,将异常包装到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进行任何额外的处理,但多线程代码需要额外的分析和配置,以确保应用程序能够退出或崩溃。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章