大对象堆和来自队列的字符串对象

11
我有一个 Windows 控制台应用程序,它应该在数天或数月内无需重新启动即可运行。该应用程序从 MSMQ 中检索“工作”并对其进行处理。有 30 个线程同时处理工作块。
来自 MSMQ 的每个工作块大约为 200KB,其中大部分分配在单个字符串对象中。
我注意到,在处理约 3-4 千个这些工作块后,应用程序的内存消耗非常高,占用了 1-1.5GB 的内存。
我通过分析器运行应用程序,并注意到大部分内存(可能是一吉字节左右)未使用在大型对象堆中,但结构是分散的。
我发现这些未使用(垃圾收集)的字节中有 90% 是之前分配的字符串。然后我开始怀疑来自 MSMQ 的字符串是分配、使用然后释放的,因此是碎片化的原因。
我知道类似 GC.Collect(2 或 GC.Max...) 的事情不会有帮助,因为它们会将大对象堆 gc,但不会压缩它(这就是问题所在)。所以我认为我需要缓存这些字符串并以某种方式重新使用它们,但由于字符串是不可变的,我必须使用 StringBuilder。
我的问题是:是否有任何方式可以不更改底层结构(即使用 MSMQ,因为这是我无法更改的内容)并仍避免每次初始化新字符串以避免碎片化 LOH?
谢谢, Yannis
更新:有关当前检索这些“工作”块的方式
当前,将这些存储为 MSMQ 中的 WorkChunk 对象。每个对象都包含一个名为 Contents 和另一个名为 Headers 的字符串。这些是实际的文本数据。如果需要,我可以更改存储结构,并且可能将底层存储机制更改为其他东西而不是 MSMQ。
在工作节点侧,我们目前执行:
WorkChunk chunk = _Queue.Receive();

目前我们可以缓存的内容很少。如果我们在某种程度上改变了结构,那么我想我们可能会有一些进展。无论如何,我们将不得不解决这个问题,因此我们将尽一切努力避免浪费数月的工作。

更新:我尝试了以下一些建议,并注意到在我的本地机器上(运行Windows 7 x64和64位应用程序)无法重现此问题。这使事情变得更加困难-如果有人知道原因,那么它将真正有助于在本地复制此问题。


嗨,亨克 - 请查看更新以获取有关这些工作块的更多信息。 - Yannis
但这是一个实际的问题吗?在具有>= 8GB RAM的64位PC上,1.5GB应该可以继续。 - H H
但最终由于过度分页而变得缓慢...这不是一个哲学问题 - 它每天都会发生! - Yannis
1
你能偶尔停止接受工作,完成所有正在运行的工作(释放所有对象),运行GC.Collect(),然后再开始接受工作吗? - Joey
是的 - 你认为这可能行得通吗?有几个DoWork线程在几个这样的应用程序上运行。因此,暂时拒绝在其中一个应用程序上工作并不是问题。 - Yannis
显示剩余3条评论
4个回答

4
您的问题似乎是由于大对象堆上的内存分配引起的——大对象堆没有被压缩,因此可能会导致碎片化。这里有一篇很好的文章,详细介绍了一些调试步骤,可以跟踪确认大对象堆的碎片化是否正在发生: Large Object Heap Uncovered 您似乎有三个解决方案:
1. 修改应用程序以处理块/较短字符串,其中每个块小于85,000字节——这避免了大对象的分配。 2. 修改应用程序以预先分配一些大块内存,并通过将新消息复制到已分配的内存中来重复使用这些块。请参见Heap fragmentation when using byte arrays。 3. 将事情保持原样——只要您不遇到内存不足异常,并且应用程序不干扰系统上运行的其他应用程序,您应该将事情保持原样。
重要的是要理解虚拟内存和物理内存之间的区别 - 即使进程使用大量虚拟内存,如果分配的对象数量相对较低,则该进程的物理内存使用可能很低(未使用的内存被分页到磁盘),这意味着对系统上的其他进程影响较小。您还可以发现“VM Hoarding”选项有所帮助 - 阅读“揭示大对象堆”文章以获取更多信息。
任何一种更改都涉及将应用程序更改为使用字节数组和短子字符串来执行其部分或全部处理,而不是单个大字符串 - 这对您来说有多难取决于您正在进行的处理类型。

谢谢Justin。问题在于这些字符串来自不同的系统,通过消息队列传输。所以我目前无法说“获取那个工作块的一半”,除非我改变整体存储结构——我想这就是我需要想法和建议的地方。 - Yannis
@Yannis 如果你想修改你的应用程序,那么看起来是这样的 - 对于你可能想要做的建议,可能需要更详细地了解正在进行的处理类型。你看到我的最新编辑了吗?你应该考虑到你所看到的这种行为可能是完全正常的(只要你不会遇到OOM异常,这是32位还是64位进程?) - Justin
Justin - 这是一个64位进程,结果导致计算机(Windows 2008 Server)由于过多的分页而变得缓慢。这是有道理的。让我问一下:如果我将String Contents属性更改为包含85k char块的char[][] char数组 - 是否有帮助? - Yannis
@Yannis 是的,那就是我所说的,但你需要小心,确保CLR不会将多维数组视为单个分配,数组列表可能更好。另外要注意 sizeof(char) == 2 - Justin
我知道List在幕后使用数组。也许在这种情况下,LinkedList<char[]>会更好。我会在周末尝试一下,但测试这些东西真的很麻烦。 - Yannis

2
当LOH上有碎片时,意味着有对象被分配在其中。如果你可以容忍延迟,偶尔等待所有当前运行的任务完成并调用GC.Collect()。当没有引用大对象时,它们都将被收集,有效地消除了LOH的碎片。当然,这仅适用于(几乎)所有大对象都未被引用的情况。
此外,转换到64位操作系统也可能有所帮助,因为由于碎片化而导致的内存不足问题在64位系统上不太可能成为问题,因为虚拟空间几乎是无限的。

Steven,我认为你错了,因为碎片化并不意味着对象在LOH中存在,而是它们曾经存在过,最终被释放,从而在LOH中留下一个空块。这意味着如果有一个120k的块(比如说),我们正在尝试分配121k,那么这将被分配到第一个可用的连续的121k字节块,从而留下120k块为空。GC.Collect()不幸的是只会释放LOH对象(为此需要GC.Collect(GC.MaxGeneration)),而不会压缩LOH。 - Yannis
1
我认为Steven并没有说GC.Collect会压缩,他是在说当你只有几个对象时调用它。这样它就会清除掉之间的大对象,留下一个相对干净的状态。 - Joey
1
@Yannis:我的意思是:一个空的 LOH 无法被分段。Joey 表达得很好。 - Steven
现在明白了 - 最初的误解向你道歉。我也会尝试一下。 - Yannis
1
@Yannis:沟通不畅常常是由发件人引起的。无需道歉。 - Steven

1

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

一旦在 LOH 中创建了大型对象,就无法删除(据我所知),因此如果无法避免创建这些对象,则最好的计划是重用它们。

如果您可以在两端更改协议,则将“内容”字符串缩小为一组较小的字符串(每个小于80k)应该可以防止它们被存储在 LOH 中。


这就是原帖中已经提到的。但是,如何重复使用一个字符串? - H H
Tony - 问题在于将这些内容序列化并在另一端进行反序列化。无论我做什么,这个对象都会以某种方式包含这些“内容” - 即使是以小块的形式。 - Yannis
你能在不将数据保存在单个字符串中的情况下处理它吗?也就是说,你能够遍历小块并“消耗”它们,而无需先进行连接吗? - tonycoupland
是的 - 我在考虑创建char[][]的块,即创建尽可能多的char数组块来存储字符串内容,以便将对象放置在LOH上的85k限制以下。你认为这样会行吗? - Yannis
我们实现了类似的东西,但是使用了List<> of List<>s,在达到阈值时避免将对象放入LOH,所以我认为分块成char[]应该可行。 - tonycoupland

0

使用String.Intern(...)来消除重复引用怎么样?这会有一定的性能损失,但根据你的字符串可能会产生影响。


如果您能将标题和内容分解为键/值对,并对所有键和值执行.Intern操作,那么它会更有效。然后,您将不会有重复的数据,但是会得到一个不同的数据结构,可能需要更多的处理。 - Jesper Madsen

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