捕获java.lang.OutOfMemoryError?

126

文档java.lang.Error的文档说:

Error是Throwable的子类,表示应用程序不应该尝试捕获的严重问题

但是由于java.lang.Errorjava.lang.Throwable的子类,我可以捕获这种类型的Throwable。

我理解为什么捕获这种异常不是一个好主意。据我所知,如果我们决定捕获它,catch处理程序不应该自己分配任何内存。否则会再次抛出OutOfMemoryError

所以,我的问题是:

  1. 是否有任何现实场景下捕获java.lang.OutOfMemoryError可能是个好主意?
  2. 如果我们决定捕获java.lang.OutOfMemoryError,如何确保catch处理程序不会自己分配任何内存(是否有任何工具或最佳实践)?

3
类似的问题:https://dev59.com/LnI-5IYBdhLWcg3wwLW3 和 https://dev59.com/iXRC5IYBdhLWcg3wRO3k - BalusC
1
对于您的第一个问题,我会添加捕获OutOfMemoryError以便(至少尝试)通知用户出现了问题。之前,错误没有被catch(Exception e)子句捕获,用户也没有得到任何反馈。 - Josep Rodríguez López
1
有一些特定的情况,例如分配一个巨大的数组,可以在该操作周围捕获OOM错误并恢复得相当好。但是,在一个大块代码周围放置try/catch并尝试清理和继续可能是一个不好的想法。 - Hot Licks
请参见https://dev59.com/Fm3Xa4cB1Zd3GeqPdVIs。 - Raedwald
14个回答

100
有许多情况下,您可能希望捕获OutOfMemoryError异常,在我个人的经验中(在Windows和Solaris JVM上),仅极少数情况下OutOfMemoryError会导致JVM崩溃。
只有一个好的理由可以捕获OutOfMemoryError,那就是要优雅地关闭,清除资源并记录失败原因(如果仍然可能这样做的话)。
通常,OutOfMemoryError是由于无法满足剩余堆资源的块内存分配而引起的。
当抛出Error时,堆包含与不成功分配之前相同数量的已分配对象,现在是丢弃对运行时对象的引用以释放更多可能需要进行清理的内存的时候了。在这些情况下,可能仍然可以继续,但这绝对是个坏主意,因为你永远无法100%确定JVM是否处于可修复状态。
演示在catch块中OutOfMemoryError不意味着JVM已耗尽内存:
private static final int MEGABYTE = (1024*1024);
public static void runOutOfMemory() {
    MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
    for (int i=1; i <= 100; i++) {
        try {
            byte[] bytes = new byte[MEGABYTE*500];
        } catch (Exception e) {
            e.printStackTrace();
        } catch (OutOfMemoryError e) {
            MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage();
            long maxMemory = heapUsage.getMax() / MEGABYTE;
            long usedMemory = heapUsage.getUsed() / MEGABYTE;
            System.out.println(i+ " : Memory Use :" + usedMemory + "M/" +maxMemory+"M");
        }
    }
}

以下是此代码的输出结果:

1 : Memory Use :0M/247M
..
..
..
98 : Memory Use :0M/247M
99 : Memory Use :0M/247M
100 : Memory Use :0M/247M

如果运行的是一些关键任务,我通常会捕获Error,将其记录到syserr,并使用我所选择的日志框架进行记录,然后清理资源并以清晰的方式关闭。最坏的情况是什么? JVM已经崩溃(或已经死亡),通过捕获Error,至少有机会清理。

需要注意的是,您必须只在可以进行清理的地方针对这些类型的错误进行捕捉。不要像catch(Throwable t){} 这样无意义地覆盖所有地方。


同意,我会在一个新的回答中发布我的实验。 - Mister Smith
4
“你永远无法百分之百确定JVM处于可修复状态”:因为“OutOfMemoryError”可能是从一个将程序置于不一致状态的点抛出的,而它可以在任何时间被抛出。请参见https://dev59.com/omoy5IYBdhLWcg3wKq-s - Raedwald
在OpenJdk1.7.0_40中,当我运行这段代码时,没有任何错误或异常。即使我将MEGABYTE更改为GIGABYTE(1024 * 1024 * 1024),也是如此。这是因为优化器删除了变量“byte [] bytes”,因为它在代码的其余部分中没有使用吗? - RoboAlex
"有许多情况下,您可能希望捕获OutOfMemoryError错误"与"只有一个很好的理由可以捕获OutOfMemoryError错误"。做出你的决定!" - Stephen C
@Chris,是的,Stackoverflow的用户界面可用性并不是非常出色。 - Pacerier
3
可能需要捕获OutOfMemory错误的实际情况是:尝试分配超过2G元素的数组时。 在这种情况下,错误名称有点不准确,但仍然是OOM。 - Charles Roth

31

可以从中恢复:

package com.stackoverflow.q2679330;

public class Test {

    public static void main(String... args) {
        int size = Integer.MAX_VALUE;
        int factor = 10;

        while (true) {
            try {
                System.out.println("Trying to allocate " + size + " bytes");
                byte[] bytes = new byte[size];
                System.out.println("Succeed!");
                break;
            } catch (OutOfMemoryError e) {
                System.out.println("OOME .. Trying again with 10x less");
                size /= factor;
            }
        }
    }

}

但这有意义吗?你还想做什么?为什么最初要分配那么多内存?使用更少的内存也可以吗?为什么不早点利用它呢?或者如果不可能,为什么不从一开始就给JVM更多的内存呢?

回到你的问题:

1:是否存在真正的场景,捕获 java.lang.OutOfMemoryError 可能是一个好主意?

我想不到任何场景。

2:如果我们捕获java.lang.OutOfMemoryError,如何确保 catch 处理程序本身不会分配任何内存(有哪些工具或最佳实践)?

这取决于导致 OOME 的原因。如果它在try块之外被声明并且逐步发生,那么您的机会很小。您可以预先保留一些内存空间:

private static byte[] reserve = new byte[1024 * 1024]; // Reserves 1MB.

然后在 OOME 期间将其设置为零:

} catch (OutOfMemoryException e) {
     reserve = new byte[0];
     // Ha! 1MB free!
}

当然这样做没有任何意义;)只需要根据你的应用程序需求为JVM分配足够的内存。必要时运行分析器。


2
即使保留空间也不能保证工作解决方案。因为该空间可能会被其他线程占用 ;) - Wolph
3
第一段代码片段可以正常工作是因为触发错误的对象是一个单独的大对象(数组)。当达到 catch 语句时,它已被占用大量内存的 JVM 回收。如果在 try 块之外、其他线程或同一 catch 中使用相同的对象,则 JVM 不会回收它,这将使得无法创建任何类型的新单个对象。例如,第二个代码片段可能无法正常工作。 - Mister Smith
4
为什么不直接将其设为 null - Pacerier
1
你不能依赖垃圾回收器立即释放那个数组。特别是在 OOME 抛出后处于如此脆弱的状态下。你不能立即在接下来的行中使用那块内存,对吧? - Andrew Gallasch
1
@MisterSmith 您的评论毫无意义。该大对象不存在。它一开始就没有被分配:它触发了OOM,因此肯定不需要GC。 - user207421
显示剩余6条评论

19

一般来说,试图捕获和恢复OOM并不是一个好主意。

  1. 其他线程也可能抛出OOM异常,包括您的应用程序甚至不知道的线程。这些线程现在都已经死亡,任何等待notify的操作都可能永远被卡住。简而言之,您的应用程序可能已经无法修复。

  2. 即使您成功地恢复了,您的JVM仍然可能遭受堆饥饿的问题,因此您的应用程序的性能将大为降低。

对于OOM,最好的处理方式是让JVM崩溃。

(这假设JVM确实会崩溃。例如,在Tomcat servlet线程上发生OOM不会使JVM崩溃,这会导致Tomcat进入一种无法响应任何请求的定态状态...即使是要求重新启动的请求)

编辑

我并不是说根本不应该捕获OOM。问题出现在您尝试从OOM中恢复,无论是故意还是由于疏忽。每当捕获OOM(直接或作为Error或Throwable的子类型)时,您都应该重新抛出它或安排应用程序/JVM退出。

另外:这表明,为了在面对OOM时最大程度地保证健壮性,应用程序应该使用Thread.setDefaultUncaughtExceptionHandler()设置一个处理程序,以便在任何线程上抛出OOM时导致应用程序退出。我很想听听您的意见...

唯一的其他情况是您确信OOM没有造成任何副作用,即您知道:

  • 具体是什么引起了OOM,
  • 应用程序在进行什么操作时发生OOM,并且可以简单地丢弃该计算,
  • 确保(大致)同时发生的OOME不可能发生在另一个线程上。

有些应用程序可以知道这些事情,但对于大多数应用程序而言,您无法确定在OOME之后继续操作是否安全。即使在尝试时它表现得很好。

问题在于需要正式证明“预期”的OOME的后果是安全的,并且“未预期”的OOME不能在try/catch OOME的控制范围内发生。


是的,我同意你的观点。一般来说,这是一个不好的想法。但是为什么我还有可能捕捉到它呢? :) - Denis Bazhenov
@dotsid - 1) 因为有些情况下你应该捕获它,2) 因为不可能捕获OOM会对Java运行时的语言和/或其他部分产生负面影响。 - Stephen C
1
你说:“因为有时候你需要捕获它”。所以这是我最初问题的一部分。什么情况下你想要捕获OOME? - Denis Bazhenov
1
@dotsid - 请查看我的编辑答案。我能想到唯一需要捕获OOM的情况是当你需要这样做来强制一个多线程应用在OOM事件中退出。您可能希望对所有Error子类型执行此操作。 - Stephen C
1
仅仅捕获 OOME 是不够的,你还需要从中恢复。如果一个线程本应该(比如说)通知另一个线程,但是它却遇到了 OOME,那么这个线程该如何恢复呢?当然,JVM 不会崩溃。但是由于线程被卡在等待从因捕获 OOME 而重新启动的线程发来的通知上,应用程序很可能会停止工作。 - Stephen C
显示剩余5条评论

16

是的,有现实世界的场景。这是我的场景:我需要在内存受限的集群上处理非常多的数据集。给定的JVM实例依次处理许多项目,但其中一些项目太大了,无法在集群上处理:我可以捕获OutOfMemoryError并记录哪些项目太大了。稍后,我可以在具有更多RAM的计算机上仅重新运行大型项目。

(由于是单个多GB分配的数组失败,捕获错误后JVM仍然正常,并且有足够的内存来处理其他项目。)


所以你有像 byte[] bytes = new byte[length] 这样的代码?为什么不在早期检查 size 呢? - Raedwald
1
因为相同的“size”会占用更多的内存。我通过异常处理,因为在大多数情况下一切都会很好。 - Michael Kuhn

10

有些情况下,捕捉 OOME 是有意义的。例如,IDEA 捕获它们并弹出一个对话框让你更改启动内存设置(然后在完成后退出)。应用服务器可能会捕获并报告它们。关键是在分派的高级别上执行此操作,以便在捕获异常时有合理的机会释放大量资源。

除了上面的 IDEA 场景外,在一般情况下,捕获应该是 Throwable,而不仅仅是 OOM,并且应该在至少线程将要很快终止的上下文中进行。

当然,大多数情况下,内存枯竭且情况不可恢复,但也有一些情况是有道理的。


8
我看到这个问题,因为我在想在我的情况下是否捕获OutOfMemoryError是一个好主意。我在这里回答部分原因是要展示另一个例子,即在某些情况下(比如我),捕获此错误确实是有意义的;另一方面,我也想知道在我的情况下是否是一个好主意(作为一名初级开发人员,我对自己编写的任何一行代码都不能太肯定)。
无论如何,我正在开发一个Android应用程序,可以在不同内存大小的设备上运行。危险的部分是从文件解码位图并在ImageView实例中显示它。我不想限制更强大的设备在解码位图的大小方面,也不能确定该应用程序不会在某些我从未接触过的非常低内存的古老设备上运行。因此,我做了这个:
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options(); 
bitmapOptions.inSampleSize = 1;
boolean imageSet = false;
while (!imageSet) {
  try {
    image = BitmapFactory.decodeFile(filePath, bitmapOptions);
    imageView.setImageBitmap(image); 
    imageSet = true;
  }
  catch (OutOfMemoryError e) {
    bitmapOptions.inSampleSize *= 2;
  }
}

我通过这种方式,为更多和性能不同的设备提供服务,以满足它们或者说它们的用户的需求和期望。


1
另一个选择是计算您可以处理的位图大小,而不是尝试并失败。 "异常情况应该使用异常" - 我想有人这样说过。但我会说,您的解决方案似乎是最简单的出路,也许不是最好的,但可能是最简单的。 - jontejj
这取决于编解码器。想象一下,一个10MB的bmp可能只会导致略微超过10MB的堆,而一个10MB的JPEG将会“爆炸”。在我的情况下,我想解析一个XML,其复杂性取决于内容的复杂程度,可能会有很大的变化。 - Daniel Alder

6

我有一个应用程序需要从OutOfMemoryError故障中恢复,而在单线程程序中总是有效的,但在多线程程序中有时无法正常工作。该应用程序是一个自动化Java测试工具,它执行生成的测试序列以达到测试类的最大可能深度。现在,UI必须稳定,但是在增加测试用例树时,测试引擎可能会耗尽内存。我通过以下代码惯用语来处理测试引擎中的问题:

boolean isOutOfMemory = false;  // 用于报告的标志
try {
   SomeType largeVar;
   // 分配更多largeVar的主循环
   // 可能正常终止,也可能抛出OutOfMemoryError
}
catch (OutOfMemoryError ex) {
   // largeVar现在已经超出范围,所以是垃圾
   System.gc();                // 清除largeVar数据
   isOutOfMemory = true;       // 标志可供使用
}
// 程序测试标志以报告恢复情况

这在单线程应用程序中每次都有效。但最近,我将我的测试引擎放入了与UI不同的工作线程中。现在,内存不足可能在任何一个线程中任意发生,并且对于如何捕获它并不清楚。

例如,当我的UI中的动画GIF的帧正在被一个由Swing类在幕后创建的专有线程循环时,我遇到了OutOfMemoryError。我原以为我已经提前分配了所有所需的资源,但显然动画程序每次获取下一张图片时都在分配内存。如果有人对如何处理在任何线程中引发的OutOfMemoryError有想法,我很乐意听取建议。


在单线程应用程序中,如果您不再使用某些创建出错的问题新对象,这些对象可以在catch子句中被收集。但是,如果JVM检测到该对象可能稍后会被使用,则无法进行收集,应用程序将崩溃。请参见我在此线程中的答案。 - Mister Smith

5

是的,真正的问题是“在异常处理程序中你要做什么?” 对于几乎所有有用的操作,您都需要分配更多的内存。如果您想在发生OutOfMemoryError时进行一些诊断工作,可以使用HotSpot VM提供的-XX:OnOutOfMemoryError=<cmd>钩子。它会在发生OutOfMemoryError时执行您的命令,您可以在Java堆之外执行一些有用的操作。您确实希望在应用程序首次运行内存不足时就防止其发生,因此找出原因是第一步。然后您可以根据需要增加MaxPermSize的堆大小。以下是一些其他有用的HotSpot钩子:

-XX:+PrintCommandLineFlags
-XX:+PrintConcurrentLocks
-XX:+PrintClassHistogram

完整列表请点击此处


这比您想象的还要糟糕。因为 OutOfMemoryError 可以在程序的任何时刻被抛出(不仅仅是从 new 语句),当您捕获该异常时,程序将处于未定义状态。 - Raedwald

4

虽然可以捕获OOME,但通常情况下这是无用的,这取决于JVM是否能够在到达catch块时进行垃圾回收,并且此时还剩多少堆内存。

例如,在我的JVM中,该程序可以正常运行至完成:

import java.util.LinkedList;
import java.util.List;
                
public class OOMErrorTest {             
    public static void main(String[] args) {
        List<Long> ll = new LinkedList<Long>();
            
        try {
            long l = 0;
            while(true){
                ll.add(new Long(l++));
            }
        } catch(OutOfMemoryError oome){         
            System.out.println("Error catched!!");
        }
        System.out.println("Test finished");
    }  
}

然而,只需在catch块中添加一行,即可理解我的意思:

import java.util.LinkedList;
import java.util.List;
                
public class OOMErrorTest {             
    public static void main(String[] args) {
        List<Long> ll = new LinkedList<Long>();
            
        try {
            long l = 0;
            while(true){
                ll.add(new Long(l++));
            }
        } catch(OutOfMemoryError oome){         
            System.out.println("Error caught!!");
            System.out.println("size:" +ll.size());
        }
        System.out.println("Test finished");
    }
}

第一个程序运行良好,因为当到达catch块时,JVM检测到列表不再被使用(此检测也可以是在编译时进行的优化)。因此,当我们到达打印语句时,堆内存已经几乎完全释放,所以我们现在有了广泛的操作余地。这是最好的情况。
然而,如果代码安排得像第二个片段一样,列表“ll”在OOME被捕获后被使用,JVM将无法收集它。 OOME由新的Long创建引发,但很快我们就要创建一个新的对象(在System.out.println行中是一个字符串),堆几乎满了,因此会抛出新的OOME。这是最坏的情况:我们尝试创建一个新对象,失败了,我们捕获了OOME,但现在需要新的堆内存(例如:创建新对象)的第一条指令将抛出新的OOME。想想看,在这种情况下,我们还能用那么少的内存做什么?可能只剩下退出,因此我说这是无用的。
JVM未垃圾回收资源的原因之一真的很可怕:与其他线程共享资源并且也在使用该资源。任何人都可以看到,如果将其添加到任何非实验性应用程序中,捕获OOME会有多么危险。
我正在使用Windows x86 32位JVM(JRE6)。每个Java应用程序的默认内存为64MB。

如果我在catch块中执行ll=null会怎么样? - Naanavanalla

3

我能想到唯一的捕获OOM错误的原因就是你有一些庞大的数据结构你不再使用,可以将其设置为null并释放一些内存。但这意味着你正在浪费内存,应该修复代码而不是在产生OOM错误后勉强运行。(2)即使你成功捕捉了OOM错误,你又能做什么呢?OOM错误可能会在任何时候发生,潜在地留下一切都未完成的情况。


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