【问题标题】:.NET OutOfMemoryException.NET OutOfMemoryException
【发布时间】:2011-05-14 00:39:44
【问题描述】:

为什么会这样:

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++;
        }
    }
}

有什么区别?

【问题讨论】:

标签: c# .net out-of-memory


【解决方案1】:

您是否在文档中查找过int.MaxValue?它相当于 2GB,这可能比您可用于连续的“a”字符块更多的 RAM - 这就是您在这里所要求的。

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

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

当我使用这个更大的字符串大小运行时,我在 68 次循环后的 10 秒内得到了异常(检查 i)。

【讨论】:

  • 是的。我理解这个事实。我来自 JAVA 世界,如果没有更多的系统内存可供分配,VM 会尖叫停止。但是在 .net 中,尤其是第二个示例...我实际上可以使系统进入无响应状态并且 VM 永远不会发出噪音...这是怎么回事?
  • 因此,在 .Net 中,您将获得一个 OutOfMemoryException
  • 在 JAVA 中,我无法无休止地将新字符串添加到列表中,JVM 确实会给出错误...要么它是一个非常大的字符串(如案例 1),要么将许多较小的字符串添加到列表中...在这两种情况下,JVM 都会给出错误。
  • 相信我,如果你让它运行足够长的时间,.Net 也会呕吐。您的默认 JVM RAM 池设置为多少?我在 64MB 左右看到了这个 - 我希望你有比这更多的 RAM 在你的 q 中的第二个 sn-p 中一次吃掉 1K。尝试在 .Net 中增加更大的增量,看看需要多长时间。
  • 这种行为也让我感到困惑,因此我来找你们……我只是在玩 .NET,崩溃很有趣。 JVM 给了我 -Xmx 和 -Xms 参数来玩它,我可以让它迟早咳嗽......所以这告诉我的是 JVM never 进入 HDD 分页??? ?
【解决方案2】:

你的

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

抛出 OutOfMemoryException 仅仅是因为 .NET 的 string 有长度限制。 MSDN docs 中的“备注”部分说:

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 的 great article

【讨论】:

    【解决方案3】:

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

    话虽如此,这样大小的 ArrayList 应该会使您内存不足,但您可能没有让代码运行足够长的时间使其内存不足。

    【讨论】:

    • 其实字符串ctor不会使用共享池。
    • -1 这是运行时生成的字符串,不会被实习。
    • 我确实让它运行了......事实上,最初我已经运行了 prog,在随后的分配之间没有任何延迟,我的计算机在不到 10 秒内停止响应......
    • SLAks,chibacity:你说得对,我的假设是编译器足够聪明,可以识别出参数是恒定的,从而优化它以自动实习。
    【解决方案4】:

    因为int.MaxValue 是 2,147,483,647,即 2 GB,需要连续分配。

    在第二个例子中,操作系统每次只需要找到 1024 字节分配,就可以交换到硬盘。我敢肯定,如果你让它运行的时间足够长,你最终会在一个黑暗的地方:)

    【讨论】:

    • 我确实进入了一个(非常)黑暗的地方 :) 虚拟机永远不会告诉我我将用完堆吗?我可以将许多小变量添加到内存中......永远?
    【解决方案5】:

    在您的第一个示例中,您尝试一次创建一个 2g 字符串

    在第二个示例中,您不断将 1k 添加到数组中。您将需要循环超过 200 万次才能达到相同的消耗量。

    而且它也不是一次全部存储在一个变量中。因此,我认为您的一些内存使用量可以持久化到磁盘以为新数据腾出空间。

    【讨论】:

      【解决方案6】:

      因为单个对象cannot have more than 2 GB

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

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

      我不知道 ArrayList 在内部是如何工作的,但是您分配了多个 2 GB 的对象,并且 ArrayList - 据我所知 - 仅保存 4 个(x64 上为 8 个?)字节的指针,无论对象有多大他们指向的是。

      引用another article

      此外,具有对其他对象的引用的对象仅存储该引用。因此,如果您有一个对象包含对其他三个对象的引用,则内存占用量只有 12 个额外字节:一个指向每个被引用对象的 32 位指针。引用的对象有多大并不重要。

      【讨论】:

        【解决方案7】:

        第二个 sn-p 也会崩溃。它只需要更长时间,因为它消耗内存的速度要慢得多。请注意您的硬盘访问指示灯,当 Windows 将页面从 RAM 中取出以腾出空间时,它会疯狂地闪烁。第一个字符串构造函数立即失败,因为堆管理器不允许您分配 4 GB。

        【讨论】:

        • @Moo:字符有两个字节宽。
        • 正确的是。我试图指出我的程序让我的机器运行的方式。情况一,简单的 BAM!崩溃,我看到你的内存不足异常情况,它进入 HDD 分页等,让我的系统无响应,因为没有程序可以正确分页!甚至我的任务管理器都没有……当我让它足够长的时间时,VM 并没有启动和终止,而是我的系统只是空白 :)
        • 它被称为'垃圾'。如果机器没有大量 RAM 或硬盘速度较慢或页面文件碎片严重,则页面错误确实会使机器几乎无法使用。
        【解决方案8】:

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

        第二个版本需要更长的时间,但是由于以下几个因素,要达到 OOM 条件会发生很多变化:

        • 您将分配数百万个小对象,这些小对象都可以被 GC 访问。一旦开始给系统施加压力,GC 将花费大量时间扫描包含数百万对象的世代。这将花费大量时间并开始对分页造成严重破坏,因为冷内存和热内存将随着世代的扫描,不断地进出页面。

        • 当 GC 逐代扫描数百万个对象以尝试释放内存时,将会出现页面抖动。扫描会导致大量内存不断被调入和调出。

        抖动会导致系统停止处理开销,因此需要很长时间才能达到 OOM 条件。大部分时间将花在 GC 和第二个版本的分页上。

        【讨论】:

          【解决方案9】:

          您的系统可能会停止的一个原因是 .NET 的代码运行得更接近于金属,并且您处于一个紧密的循环中,如果进程优先级允许它应该消耗 100% 的 CPU。如果您想防止应用程序在执行紧密循环时消耗过多的 CPU,您应该在循环末尾添加类似 System.Threading.Thread.Sleep(10) 的内容,这将强制将处理时间留给其他线程。

          JVM 和 .NET 的 CLR(公共语言运行时)之间的一个主要区别是 CLR 不限制 x64 系统/应用程序上的内存大小(在 32 位应用程序中,没有大地址感知标记操作系统限制由于寻址限制,任何应用程序到 2GB)。 JIT 编译器为您的处理架构创建本机 Windows 代码,然后在与任何其他 Windows 应用程序运行的范围相同的范围内运行它。 JVM 是一个更加独立的沙箱,它根据配置/命令行开关将应用程序限制为指定的大小。

          至于两种算法的区别:

          在具有足够连续内存以分配包含 int.MaxValue 字符所需的 4GB 的 x64 环境中运行时,不能保证单个字符串创建会失败(.NET 字符串默认为 Unicode,每个字符需要 2 个字节)。 32 位应用程序总是会失败,即使设置了大地址感知标志,因为最大内存仍然是 3.5GB)。

          您的代码的 while 循环版本可能会消耗更多的整体内存,前提是您有足够的可用内存,然后抛出异常,因为您的字符串可以分配在较小的片段中,但它保证最终会遇到错误(尽管如果您有足够的资源,它可能是由于 ArrayList 超过了数组中元素的最大数量而不是无法为小字符串分配新空间而发生的)。 Kent Murra 关于字符串实习也是正确的。您将需要随机化字符串的长度或字符内容以避免实习,否则您只是创建指向同一字符串的指针。 Steve Townsend 增加字符串长度的建议也会使找到足够大的连续内存块变得更加困难,这将使异常更快地发生。

          编辑:

          我想我会提供一些人们可能会发现有助于理解 .NET 内存的链接:

          这两篇文章有点老,但是深度阅读很好:

          Garbage Collection: Automatic Memory Management in the Microsoft .NET Framework

          Garbage Collection Part 2: Automatic Memory Management in the Microsoft .NET Framework

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

          So, what’s new in the CLR 4.0 GC?

          CLR 4.5: Maoni Stephens - Server Background GC

          这个 SO Question 可以帮助您观察 .NET 内存的内部工作原理:

          .NET Memory Profiling Tools

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2012-03-25
            • 1970-01-01
            • 1970-01-01
            • 2015-11-28
            • 2016-12-27
            • 1970-01-01
            • 2011-08-14
            相关资源
            最近更新 更多