如何捕获OutOfMemoryException异常?

29

我对于我们可以通过try/catch块来捕获OutOfMemoryException有一点困惑。

给定以下代码:

Console.WriteLine("Starting");

for (int i = 0; i < 10; i++)
{
    try
    {
        OutOfMemory();
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception.ToString());
    } 
}

try
{
    StackOverflow();
}
catch (Exception exception)
{
    Console.WriteLine(exception.ToString());
}

Console.WriteLine("Done");

我用来制造OutOfMemory和StackOverflow异常的方法:

public static void OutOfMemory()
{
    List<byte[]> data = new List<byte[]>(1500);

    while (true)
    {
        byte[] buffer = new byte[int.MaxValue / 2];

        for (int i = 0; i < buffer.Length; i++)
        {
            buffer[i] = 255;
        }

        data.Add(buffer);
    }
}

static void StackOverflow()
{
    StackOverflow();
}

它打印了OutOfMemoryException 10次,然后由于无法处理StackOverflowException而终止。

执行程序时,RAM图看起来像这样: graph showing that memory gets allocated and released 10 times

我的问题是:我们为什么能够捕获OutOfMemoryException?在捕获后,我们可以继续执行任何想要的代码。根据RAM图所证明的,有内存被释放。运行时如何知道哪些对象可以进行垃圾收集以及哪些对象仍需进一步执行?


展示一下你的OutOfMemory()方法。我敢打赌你没有在分配内存之后保留其引用,这样GC就可以在它返回后释放它。 - Matthew Watson
没错,一旦OutOfMemory()抛出异常,GC就可以释放它分配的所有内存。 - Matthew Watson
5个回答

36

垃圾回收器会分析程序中使用的引用,可以丢弃任何未被使用的对象。

OutOfMemoryException并不意味着内存完全耗尽,它只是表示内存分配失败。如果您试图一次性分配大块内存,则可能仍有大量可用内存。

当没有足够的空闲内存进行分配时,系统会进行垃圾收集以释放内存。如果仍然没有足够的内存进行分配,就会抛出该异常。

StackOverflowException无法处理,因为它意味着堆栈已满,并且与堆不同,无法从中删除任何内容。您需要更多的堆栈空间才能继续运行将处理异常的代码,但没有更多的空间了。


6
OutOfMemoryException可能是因为您正在运行一个32位程序,因为您没有指定系统有多少内存,所以尝试将其构建为64位,并使用MemoryFailPoint以防止此问题发生。您还可以让我们了解OutOfMemory()函数中的内容,以获得更清晰的图片。另外,需要注意的是,如果您尝试分配比您拥有的“备用”内存更多的内存,则无法这样做并且会引发异常。在您使用data.Add()分配大型数组时,它会在最后一个“非法”的添加之前崩溃,因此仍有可用内存。因此,我认为在构建数组时出现问题,当您通过将400MB字节数组添加到“data”来触发2GB进程限制时,例如每个对象4个字节,大约10亿个对象的数组预计为约400MB。

提示:在 .net 4.5 之前,最大进程内存分配为2GB,在4.5之后可用更大的内存。


啊,所以它确实会耗尽内存,这让我相信你正在遇到应用程序限制,而不是系统限制。 - Paul Zahra
我对如何修复它不感兴趣,因为实际上并没有问题,这只是关于研究和理解CLR工作原理的问题。 - GameScripting
P.S. StackOverflow 不是唯一一个你捕捉不到的异常。你还有 AccessViolationException。 - Øyvind Bråthen
有趣的阅读 https://dev59.com/KHA75IYBdhLWcg3wGlIa - Paul Zahra

0

不确定这是否回答了你的问题,但是关于它如何决定清理哪些对象的(简化)解释如下:

垃圾收集器会获取程序中的每个运行线程,并标记所有顶层对象,也就是从堆栈帧(即当前执行点处本地变量指向的所有对象)可访问的所有对象以及静态字段指向的所有对象。

然后,它标记下一级对象,也就是所有先前标记对象的所有字段指向的对象。重复此步骤,直到没有新对象被标记为止。

因为 C# 不允许在正常上下文中使用指针,所以完成上述步骤后,非标记对象肯定无法被后续代码访问,因此可以安全地清理它们。

在你的情况下,如果你分配的对象没有被引用保留,那么 GC 就有机会将它们清理掉。另外,请记住,OutOfMemoryException 是指 CLR 程序的托管内存,而 GC 的工作略微超出了那个“盒子”的范围。


0

你能捕获OutOfMemoryException的原因是因为语言设计者决定让你这样做。这种情况有时候(但通常不是)是实用的,因为在某些情况下它是可以恢复的。

如果你尝试分配一个巨大的数组,你可能会得到一个OutOfMemoryException,但是那个巨大数组的内存实际上并没有被分配 - 所以其他代码仍然可以正常运行而没有问题。此外,由于异常引起的堆栈展开可能会导致其他对象变得适合进行垃圾回收,进一步增加可用内存的数量。


0

你的OutOfMemory()方法创建了一个数据结构(List<byte[]>),该数据结构是该方法范围内的局部变量。当你的执行线程在OutOfMemory方法中时,当前的堆栈帧被视为List的GC根。一旦你的线程进入catch块,堆栈帧已经弹出,列表已经成为不可访问的。因此,垃圾收集器确定它可以安全地收集列表(正如你在内存图中观察到的那样)。


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