.NET OutOfMemoryException

12

为什么会发生这样的情况:

class OutOfMemoryTest02
{
    static void Main()
    {
        string value = new string('a', int.MaxValue);
    }
}

抛出异常;但这个不会:

class OutOfMemoryTest
{
    private static void Main()
    {
        Int64 i = 0;
        ArrayList l = new ArrayList();
        while (true)
        {
            l.Add(new String('c', 1024));

            i++;
        }
    }
}

有什么区别?


第二个程序一直运行,直到我的机器不响应,我不得不强制重启它。 - ksm
值得注意的是,对于这篇文章未来的读者们,如果我理解正确的话,.net 4.5已经移除了这个限制。http://msdn.microsoft.com/en-us/library/hh285054(v=vs.110).aspx - TravisWhidden
9个回答

10

您是否在文档中查找了 int.MaxValue? 它相当于2GB,这可能比您可用的连续块的 RAM 还要多 - 这就是您在此处所请求的。

http://msdn.microsoft.com/en-us/library/system.int32.maxvalue.aspx

您的无限循环最终也会导致相同的异常(或与过多使用 RAM 相关的其他间接异常),但需要一段时间。 尝试将 1024 增加到 10 * 1024 * 1024,以便在循环情况下更快地重现症状。

当我使用此更大的字符串大小运行时,在 68 次循环(检查 i)后不到 10 秒即可获得异常。


是的,我理解这个事实。我来自JAVA世界,在没有更多系统内存可用于分配时,虚拟机会停止运行。 但在.NET中,特别是第二个例子……我几乎可以使系统处于无响应状态,而虚拟机从未发出任何声音……这是怎么回事? - ksm
因此,在 .Net 中,您将会得到一个 OutOfMemoryException 异常。 - Steve Townsend
这种行为也让我感到困惑,所以我来找你们了...我只是在玩.NET,崩溃很有趣。JVM给我-Xmx和-Xms参数来玩,我可以让它尽早咳嗽...所以这告诉我JVM从不进入HDD分页? - ksm
我不确定(不是Java程序员),但我认为如果您使用(比如)-Xms4096m,您要么会立即崩溃,要么在RAM耗尽时进行分页。 - Steve Townsend
我甚至在第二个程序周围使用了try/catch,只是让它等待...我看到的只是我的RAM被消耗掉,但我没有看到OOM。 - ksm
显示剩余3条评论

7

Your

new string('a', int.MaxValue);

由于.NET的string有长度限制,所以当字符串超出2GB或大约10亿个字符时,会抛出OutOfMemoryException异常。在MSDN文档的“Remarks”部分中提到:

String对象在内存中的最大大小为2 GB,或者大约10亿个字符。

在我的系统(.NET 4.5 x64)上,当使用new string('a', int.MaxValue/2 - 31)时会抛出异常,而使用new string('a', int.MaxValue/2 - 32)则可以正常工作。
在第二个例子中,无限循环会分配大约2048字节的块,直到操作系统无法在虚拟地址空间中再分配任何块为止。此时,也会抛出OutOfMemoryException异常。
(~2048字节=1024个字符*每个UTF-16代码点的2个字节+字符串开销字节)
请查看Eric的这篇精彩文章

5
因为int.MaxValue是2,147,483,647,即需要连续分配的2GB。 在第二个例子中,操作系统每次只需要找到1024字节来分配,并且可以交换到硬盘。如果您让它长时间运行,我相信您最终会陷入一个黑暗的境地 :)

1
我最终陷入了一个(非常)黑暗的地方 :)虚拟机永远不会告诉我堆空间即将耗尽吗?我可以无限添加许多小变量到内存中...永远? - ksm

4

String对象可以使用共享字符串池来减少内存使用。在前一种情况下,您正在生成一个数十亿字节的字符串。在第二种情况下,编译器可能会自动将字符串打包,因此您正在生成一个1024字节的字符串,然后多次引用同一个字符串。

话虽如此,那么大的ArrayList应该会耗尽您的内存,但很可能您还没有让代码运行足够长的时间以耗尽内存。


实际上,字符串构造函数不会使用共享池。 - SLaks
-1 这是一个运行时生成的字符串,它不会被内部化。 - Tim Lloyd
我确实让它运行了...事实上,最初在连续分配之间没有延迟的情况下运行程序,我的计算机在不到10秒内停止响应... - ksm
SLAks,chibacity:你说得对,我的假设是编译器会聪明到识别参数是常量,从而将其优化为自动内部化。 - Kent Murra

3

第二段代码也会崩溃。由于它的内存消耗速度较慢,因此需要更长的时间。请注意硬盘访问灯,当Windows将页面从RAM中清除以腾出空间时,它会疯狂闪烁。第一个字符串构造函数会立即失败,因为堆管理器不允许你分配4GB。


1
@Moo:字符宽度为两个字节。 - SLaks
正确的,我试图指出我的程序如何让我的机器行为。在第一种情况下,简单的BAM!崩溃,我看到了一个OutOfMemoryException。在第二种情况下,它进入了HDD分页等等,并使我的系统无响应,因为没有程序可以正确地进行分页!甚至连我的任务管理器也不行...当我让它长时间运行时,虚拟机没有启动和终止,而是我的系统只是变成了空白 :) - ksm
它被称为“抖动”。如果机器没有足够的RAM或硬盘速度慢或分页文件碎片严重,页面错误确实会使机器几乎无法使用。 - Hans Passant

2

两个版本都会导致OOM异常,只是在32位机器上,当您尝试分配“单个”非常大的对象时,第一个版本会立即出现。

然而,第二个版本将需要更长的时间,因为有许多因素导致OOM条件:

  • 您将分配数百万个小对象,这些对象都可以被GC访问。一旦开始对系统施加压力,GC将花费大量时间扫描具有数百万个对象的代。这将需要相当长的时间,并开始对页面进行破坏,因为冷热内存将不断地在代中扫描并进行页面换入和换出。

  • 随着GC扫描数百万代对象以释放内存,将会出现页面抖动。扫描将导致大量内存不断地进行页面换入和换出。

抖动将使系统因处理开销而停滞不前,因此OOM条件需要很长时间才能达到。第二个版本的大部分时间将花费在GC和页面换入和换出上。


1
在你的第一个示例中,你试图一次创建一个2G的字符串。
在第二个示例中,你不断地向数组添加1k。你需要循环超过200万次才能达到相同的消耗量。
而且它也不是一次性全部存储在一个变量中。因此,你的一些内存使用可以持久化到磁盘上,以腾出空间存储新数据,我想。

1

因为单个对象不能超过2GB

首先,一些背景知识;在2.0版本的.Net运行时(CLR)中,我们有意做出了一个设计决策,即使在64位版本的运行时中,GC堆中允许的最大对象大小也保持为2GB。

在你的第一个示例中,你尝试分配一个2GB大小的对象,加上对象开销(8个字节?)它太大了。

我不知道ArrayList在内部是如何工作的,但你分配了多个大小为2GB的对象,而据我所知,ArrayList只存储指针,这些指针只占4字节(在x64上是8字节),无论它们指向的对象有多大。

引用另一篇文章

此外,持有对其他对象引用的对象仅存储引用。因此,如果您有一个持有对其他三个对象引用的对象,则内存占用量仅增加12个字节:每个引用对象都有一个32位指针。被引用对象的大小不影响内存占用量。

0
你的系统出现停顿的一个原因可能是因为.NET的代码运行更加接近底层,而你处于一个紧密循环中,应该占用100%的处理器资源,前提是进程优先级允许。如果你想在执行紧密循环时防止应用程序占用过多的处理器资源,可以在循环结束时添加类似System.Threading.Thread.Sleep(10)的内容,这将强制让处理器时间分配给其他线程。
JVM和.NET的CLR(通用语言运行时)之间的一个主要区别在于,CLR不会限制在x64系统/应用程序上的内存大小(在32位应用程序中,如果没有启用大地址标志,则由于寻址限制,操作系统会限制任何应用程序的大小为2GB)。JIT编译器为处理器架构创建本机Windows代码,然后在与任何其他Windows应用程序相同的范围内运行。JVM是一个更加隔离的沙盒,它根据配置/命令行开关将应用程序限制在指定的大小。
至于两种算法之间的差异:
在x64环境下,如果有足够的连续内存来分配包含int.MaxValue个字符所需的4GB(.NET字符串默认为Unicode,每个字符需要2个字节),则不能保证单个字符串创建失败。32位应用程序将始终失败,即使设置了Large Address Aware标志,因为最大内存仍然是3.5GB左右。
您的代码中while循环版本可能会在抛出异常之前消耗更多的总内存(前提是您有足够的可用内存),因为可以将字符串分配给较小的片段,但它保证最终会出现错误(尽管如果您有足够的资源,这可能是由于ArrayList超过数组中元素的最大数量而不是无法为小字符串分配新空间)。Kent Murra也正确地指出了字符串插入;您需要随机化字符串的长度或字符内容以避免插入,否则您只是创建指向相同字符串的指针。Steve Townsend建议增加字符串长度也会使查找足够大的连续内存块更加困难,从而使异常更快地发生。
编辑:
我想提供一些链接,人们可能会发现有用来理解.NET内存:
这两篇文章有点旧,但非常深入:

在Microsoft .NET Framework中自动管理内存的垃圾回收

Garbage Collection Part 2: 在Microsoft .NET Framework中自动管理内存

这些是.NET垃圾回收开发人员的博客,提供有关较新版本的.NET内存管理的信息:

那么,CLR 4.0 GC有什么新功能?

CLR 4.5:Maoni Stephens - 服务器后台GC

这个SO问题可能会帮助您观察.NET内存的内部工作:

.NET内存分析工具


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