【问题标题】:Large object heap waste大对象堆浪费
【发布时间】:2014-10-29 17:55:41
【问题描述】:

我注意到我的应用程序内存不足的速度超出了预期。它创建了许多字节数组,每个数组都有几兆字节。但是,当我使用 vmmap 查看内存使用情况时,似乎 .NET 为每个缓冲区分配的内存远远超出了所需。准确地说,在分配 9 兆字节的缓冲区时,.NET 会创建一个 16 兆字节的堆。剩余的 7 兆字节不能用于创建另一个 9 兆字节的缓冲区,因此 .NET 会创建另外 16 兆字节。所以每个 9MB 的缓冲区浪费了 7MB 的地址空间!

这是一个在 32 位 .NET 4 中分配 106 个缓冲区后抛出 OutOfMemoryException 的示例程序:

using System.Collections.Generic;

namespace CSharpMemoryAllocationTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var buffers = new List<byte[]>();
            for (int i = 0; i < 130; ++i)
            {
                buffers.Add(new byte[9 * 1000 * 1024]);
            }

        }
    }
}

请注意,您可以将数组的大小增加到 16 * 1000 * 1024 并且在内存不足之前仍然分配相同数量的缓冲区。

VMMap 显示:

还要注意托管堆的总大小和提交的总大小之间几乎存在 100% 的差异。 (1737MB 与 946MB)。

在 .NET 上是否有解决此问题的可靠方法,即我是否可以强制运行时分配不超过我实际需要的内存,或者可能是可用于多个连续缓冲区的更大的托管堆?

【问题讨论】:

  • 您对完全符合您需要的内存分配的渴望被一个更大的担忧所压倒,即避免地址空间碎片的需要。这种行为完全是设计使然,没有旋钮可以调整它。无论如何都是老式的问题,这在 64 位操作系统上不是问题。
  • @HansPassant 不幸的是,这个过程必须是 32 位的。所以你基本上是说我需要编写自己的分配器?
  • 好吧,为什么不呢。或者你可以编写更智能的代码,需要更少的内存。重用这些数组。
  • @HansPassant。它们都包含实际需要的数据,因此除了将它们转储到磁盘之外,也没有什么可做的。
  • 您可以将缓冲区分配为 7 个块(即new byte[7 * 9 * 1000 * 1024]),这非常接近 64M。

标签: c# .net


【解决方案1】:

CLR 在内部按段分配内存。根据您的描述,听起来 16 MB 分配是段,而您的数组是在这些段中分配的。剩余空间是保留的,在正常情况下不会真正浪费,因为它将用于其他分配。如果您没有任何适合剩余块的分配,则这些基本上是开销。

由于您的数组是使用连续内存分配的,因此您只能将其中的一个放入一个段中,因此在这种情况下会产生开销。

默认段大小为 16 MB,但如果分配大于该值,CLR 将分配更大的段。我不知道细节,但例如如果我分配 20 MB Wmmap 显示我 24 MB 段。

减少开销的一种方法是尽可能根据段大小进行分配。但请记住,这些是实现细节,可能会随着 CLR 的任何更新而改变。

【讨论】:

    【解决方案2】:

    CLR 一次性从操作系统中预留了 16MB 的块,但只主动占用了 9MB。

    我相信您期望 9MB 和 9MB 放在一起。困难在于该变量现在被分成 2 个堆。

     Heap 1 = 9MB + 7MB
     Heap 2 = 2MB
    

    我们现在遇到的问题是,如果原来的 9MB 被删除,我们现在有 2 个堆我们无法整理,因为内容是跨堆共享的。

    为了提高性能,方法是将它们放在单个堆中。

    如果您担心内存使用情况,请不要担心。 .NET 的内存使用并不是一件坏事,因为如果没有人使用它,有什么问题? GC 将在某个时候启动,并且内存将被清理。 GC 只会踢其中一个

    1. 当 CLR 认为有必要时
    2. 当操作系统告诉 CLR 归还内存时
    3. 当被代码强制时

    但是内存使用,尤其是在这个例子中,不应该是一个问题。内存使用停止了 CPU 周期的发生。否则,如果它不断清理内存,你的 CPU 会很高,你的进程(以及你机器上的所有其他进程)会运行得更慢。

    【讨论】:

    • 值得注意的是,这些分配都到了大对象堆中,这意味着 GC 不会做太多的事情来整理这个堆。 LOH 碎片在 .NET 应用程序中并不少见。
    • @BrianRasmussen:当然!任何大于 85'000 字节的内容都将放在 GC 队列的后面。
    • 当我得到 OutOfMemoryException 时,内存使用率很差。 GC 无关紧要,因为我实际上正在使用这些缓冲区。我正在寻找在分配几个 ~9MB 缓冲区时不浪费 40% 的地址空间的方法——这是我的问题。
    • @Asik:您正在填充内存并保留所有变量。它不能 GC,因为该方法中的每个变量都需要保留。
    • 我知道,这就是我刚才所说的。
    【解决方案3】:

    伙伴系统堆管理算法的古老症状,其中 2 的幂用于递归地分割每个块,在二叉树中,因此对于 9M,下一个大小是 16M。如果您将阵列大小降低到 8mb,您将看到您的使用量减少了一半。不是新问题,原生程序员也能解决。

    小对象池(小于 85,000 字节)的管理方式不同,但在 9MB 时,您的数组位于大对象池中。从 .NET 4.5 开始,大对象堆不参与压缩,大对象立即提升到第 2 代。

    您无法强制执行算法,但您当然可以通过确定最有效地填充二进制段的大小来强制您的用户代码。

    如果您需要使用 9 MB 数组填充进程空间,则:

    1. 弄清楚如何节省 1MB 以将数组减少到 8MB 段
    2. 使用索引器属性编写或使用分段数组类,该类抽象出 1 或 2MB 数组段的数组。以同样的方式构建无限位域或可增长的 ArrayList。实际上,我认为其中一个容器已经这样做了。
    3. 移至 64 位

    回收伙伴系统堆的碎片部分是一种具有对数回报的优化(即,无论如何您几乎都会耗尽内存)。在某些时候,无论是否方便,您都必须迁移到 64 位,除非您的数据大小是固定的。

    【讨论】:

    • 强制运行时有多种可能的含义。我在回答中的建议是按照伙伴算法以 2 的幂分配数组,否则你只是在与它作斗争。你不能替换堆算法/GC,所以用它吧。
    • FWIW,4.5.1 添加了System.Runtime.GCSettings.LargeObjectHeapCompactionMode,但如果他实际使用内存,这并不是超级相关的。
    • @EricLaw - 真的吗?我不知道那件事。这甚至没有记录在我最新的CLR via C# 副本中。感谢您指出!
    • @EricLaw - 值得一提的是,其他运行时中的 GC 确实会移动已用内存以进行压缩。压缩的概念并不总是依赖于使用情况。但我们当然是在谈论 CLR。​​
    猜你喜欢
    • 1970-01-01
    • 2012-06-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-22
    • 2010-10-15
    • 2015-02-06
    • 1970-01-01
    相关资源
    最近更新 更多