【问题标题】:Large Object Heap and String Objects coming from a queue来自队列的大对象堆和字符串对象
【发布时间】:2011-10-14 10:08:24
【问题描述】:

我有一个 Windows 控制台应用程序,它应该可以运行数天和数月而无需重新启动。该应用程序从 MSMQ 检索“工作”并对其进行处理。有 30 个线程同时处理一个工作块。

来自 MSMQ 的每个工作块大约 200kb,其中大部分分配在单个 String 对象中。

我注意到,在处理了大约 3-4 千个这样的工作块之后,应用程序的内存消耗非常高,消耗了 1-1.5 GB 的内存。

我通过分析器运行应用程序并注意到大部分内存(可能是 gig 左右)在大型对象堆中未使用,但结构是碎片化的。

我发现这些未使用的(垃圾收集)字节中有 90% 是以前分配的字符串。我开始怀疑来自 MSMQ 的字符串被分配、使用然后解除分配,因此是碎片的原因。

我知道像 GC.Collect(2 or GC.Max...) 这样的东西不会有帮助,因为它们 gc 大对象堆但不压缩它(这是这里的问题)。所以我认为我需要缓存这些字符串并以某种方式重新使用它们,但由于字符串是不可变的,我将不得不使用 StringBuilders。

我的问题是:是否无论如何不改变底层结构(即使用 MSMQ,因为这是我无法改变的)并且仍然避免每次都初始化一个新字符串以避免分割 LOH?

谢谢, 雅尼斯

更新:关于当前如何检索这些“工作”块

目前这些在 MSMQ 中存储为 WorkChunk 对象。这些对象中的每一个都包含一个名为 Contents 的字符串和另一个名为 Headers 的字符串。这些是实际的文本数据。如果需要,我可以将存储结构更改为其他内容,如果需要,我可以将底层存储机制更改为 MSMQ 以外的其他内容。

目前我们在工作节点方面

WorkChunk 块 = _Queue.Receive();

所以在这个阶段我们可以缓存的东西很少。如果我们以某种方式改变结构,那么我想我们可以取得一些进展。无论如何,我们都必须解决这个问题,所以我们将尽一切努力避免浪费数月的工作。

更新:我继续尝试以下一些建议,并注意到无法在我的本地计算机上重现此问题(运行 Windows 7 x64 和 64 位应用程序)。这让事情变得更加困难 - 如果有人知道原因,那么它真的会帮助在本地重新解决这个问题。

【问题讨论】:

  • 您如何接收这些字符串?一旦它们成为字符串,您就会被卡住。如果它们来自流或字节 [],您可能有一些选择。
  • 嗨 Henk - 查看更新以获取有关这些工作块的更多信息
  • 但这是一个实际问题吗?在具有 >= 8GB RAM 的 64 位 PC 上 1.5GB 应该能够继续。
  • 但最终会由于分页过多而缓慢爬行...这不是哲学问题 - 它每天都会发生!
  • 您能否偶尔停止接受作业,完成您正在运行的所有作业(释放所有对象),运行 GC.Collect(),然后再次开始接受作业?

标签: c# .net memory-management memory-leaks large-object-heap


【解决方案1】:

您的问题似乎是由于大对象堆上的内存分配 - 大对象堆未压缩,因此可能是碎片的来源。这里有一篇很好的文章,其中包含一些调试步骤,您可以按照这些步骤来确认正在发生大对象堆的碎片:

Large Object Heap Uncovered

您似乎有两个三个解决方案:

  1. 更改您的应用程序以对块/较短的字符串执行处理,其中每个块小于 85,000 字节 - 这样可以避免分配大对象。
  2. 更改您的应用程序以预先分配一些大块内存,然后通过将新消息复制到分配的内存中来重新使用这些块。见Heap fragmentation when using byte arrays
  3. 保持原样 - 只要您没有遇到内存不足异常并且应用程序不干扰系统上运行的其他应用程序,您可能应该保持原样。

理解虚拟内存和物理内存的区别很重要——即使进程使用了​​大量的虚拟内存,如果分配的对象数量相对较少,那么可能是该进程的物理内存使用进程低(未使用的内存被分页到磁盘)意味着对系统上其他进程的影响很小。您可能还会发现“VM Hoarding”选项会有所帮助 - 请阅读“Large Object Heap Uncovered”文章了解更多信息。

任何一种更改都涉及更改您的应用程序以使用字节数组和短子字符串而不是单个大字符串来执行其部分或全部处理 - 这对您来说有多困难将取决于它是哪种处理你在做。

【讨论】:

  • 谢谢贾斯汀。问题是这些字符串通过消息队列来自不同的系统。所以我目前不能说“获得一半的工作块”,除非我改变整体存储结构——我想这就是我需要想法和建议的地方
  • @Yannis 如果你想改变你的应用程序,那么它看起来确实是这样 - 关于你可能想要如何做到这一点的建议可能需要更多关于正在完成的处理的细节.你看过我最新的编辑吗?您应该考虑到您看到的这种行为可能非常好(只要您没有收到 OOM 异常,这是 32 位还是 64 位进程?)
  • Justin - 这是一个 64 位进程,结果是计算机(Windows 2008 Server)由于分页过多而慢到爬行。这是有道理的。让我问这个问题:如果我将 String Contents 属性更改为 char[][] ,其中包含 85k 的 char 块的 char 数组(在 LOH 上放置东西的限制) - 这会有帮助吗?
  • @Yannis 是的,这就是我所说的那种事情,除了你需要注意 CLR 不只是将多维数组视为单个分配,数组列表可能更好。还要注意sizeof(char) == 2.
  • 我知道 List 在幕后使用了一个数组。在这种情况下,也许 LinkedList 会更好。周末会试一试,但测试这些东西真的很痛苦
【解决方案2】:

当 LOH 上有碎片时,表示上面有分配的对象。如果你能负担得起延迟,你可以偶尔等到所有当前正在运行的任务完成,然后打电话给GC.Collect()。当没有被引用的大对象时,它们都会被收集起来,有效地去除了LOH的碎片。当然,这仅在(几乎)所有大对象都未引用时才有效。

此外,迁移到 64 位操作系统也可能会有所帮助,因为在 64 位系统上,由于碎片导致的内存不足不太可能成为问题,因为虚拟空间几乎是无限的。

【讨论】:

  • Steven 我认为你错了,因为碎片并不意味着对象存在(在 LOH 中),但它们曾经存在并最终被取消分配,因此在 LOH 中留下了一个空块。这意味着如果有一个 120k 的块(比如说)并且我们正在尝试分配 121k,那么它将在第一个可用的 121k 字节的连续块处分配,从而使 120k 块为空。不幸的是,GC.Collect() 只会取消分配 LOH 对象(为此需要 GC.Collect(GC.MaxGeneration))而不压缩 LOH。
  • 我不认为 Steven 是说 GC.Collect 会压缩,我认为他是说当你只有几个对象时调用它。这样,它将摆脱大物体之间的间隙,从而为您留下漂亮的(ish)干净的石板。
  • @Yannis:我的意思是:一个空的 LOH 不能被碎片化。乔伊很好地改写了它。
  • 现在明白了 - 对最初的误解表示歉意。也会试一试
  • @Yannis:沟通不畅总是由发件人造成的。不需要道歉。
【解决方案3】:

也许您可以创建一个字符串对象池,您可以在处理工作时使用它,然后在完成后返回。

一旦在 LOH 中创建了一个大对象,就无法将其移除 (AFAIK),因此如果您无法避免创建这些对象,那么最好的计划是重用它们。

如果您可以在两端更改协议,那么将您的“内容”字符串减少为一组较小的字符串(每个

【讨论】:

  • OP 已经说过了。但是如何重用字符串?
  • Tony - 问题在于序列化这些内容并在另一端反序列化它们。无论我做什么,这个对象都会以一种或另一种方式包含这些“内容”——即使是小块。
  • 您能否在不将数据保存在单个字符串中的情况下处理数据?即你可以走小块并“消耗”它们而不需要先连接吗?
  • 是的 - 我正在考虑创建 char[][] 块,即存储小于 85k 限制的字符串内容所需的 char 数组块,以便将对象放在 LOH 上。你觉得这样行吗?
  • 我们实现了类似的东西,但是在达到阈值时使用 List of Lists 并且它确实避免将对象放在 LOH 上,所以是的,我认为分块到 char[] 应该可以工作
【解决方案4】:

如何使用 String.Intern(...) 消除重复引用。它有性能损失,但取决于您的字符串,它可能会产生影响。

【讨论】:

  • 如果您可以将标题和内容分成键/值对,对所有键和值执行 .Intern 会更好。然后你会得到没有重复的数据,而是一个不同的数据结构,这可能需要更多的处理。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-09-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-30
  • 2015-04-22
  • 2018-05-20
相关资源
最近更新 更多