大对象堆碎片
我正在处理的C#/。NET应用程序正在遭受缓慢的内存泄漏。 我曾经使用CDB与SOS来确定发生了什么,但是这些数据似乎没有任何意义,所以我希望你们中的一个人可能以前经历过这种情况。
该应用程序在64位框架上运行。 它正在不断地计算和串行化数据到远程主机,并且正好击中大对象堆(LOH)。 但是,大多数的LOH对象我都希望是暂时的:一旦计算完成并被发送到远程主机,内存应该被释放。 然而,我所看到的是大量的(活)对象数组与交替的空闲块,例如,从LOH中随机select一个段:
0:000> !DumpHeap 000000005b5b1000 000000006351da10 Address MT Size ... 000000005d4f92e0 0000064280c7c970 16147872 000000005e45f880 00000000001661d0 1901752 Free 000000005e62fd38 00000642788d8ba8 1056 <-- 000000005e630158 00000000001661d0 5988848 Free 000000005ebe6348 00000642788d8ba8 1056 000000005ebe6768 00000000001661d0 6481336 Free 000000005f214d20 00000642788d8ba8 1056 000000005f215140 00000000001661d0 7346016 Free 000000005f9168a0 00000642788d8ba8 1056 000000005f916cc0 00000000001661d0 7611648 Free 00000000600591c0 00000642788d8ba8 1056 00000000600595e0 00000000001661d0 264808 Free ...
很明显,如果我的应用程序在每次计算过程中都创build了长寿命,大的对象,我预计会发生这种情况。 (这样做,我接受将有一定程度的LOH碎片,但这不是问题在这里)。问题是非常小的(1056字节)对象数组,你可以看到在上面的转储,我不能在代码中看到被创造出来,并以某种方式保持着根源。
另请注意,当堆段被转储时,CDB不报告types:我不确定这是否相关。 如果我转储标记的(< – )对象,CDB / SOS报告它很好:
0:015> !DumpObj 000000005e62fd38 Name: System.Object[] MethodTable: 00000642788d8ba8 EEClass: 00000642789d7660 Size: 1056(0x420) bytes Array: Rank 1, Number of elements 128, Type CLASS Element Type: System.Object Fields: None
对象数组的元素都是string,string可以从我们的应用程序代码中识别。
此外,我无法find他们的GC根!GCRoot命令挂起,永远不会回来(我甚至试图把它留在一夜之间)。
所以,如果有人能够告诉我们为什么这些小的(<85k)的对象数组在LOH上结束的话,我将非常感激:.NET将在那里放置一个小对象数组? 另外,有没有人碰巧知道确定这些对象的根源的另一种方法?
更新1
我昨天晚些时候提出的另一个理论是,这些对象数组开始很大,但是已经被缩小,留下了在内存转储中明显存在的空闲内存块。 令我怀疑的是对象数组总是长为1056个字节(128个元素),128 * 8个引用和32个字节的开销。
这个想法是,也许在库或CLR中的一些不安全的代码正在破坏数组标题中的元素数量字段。 我知道一个远射的位
更新2
感谢布莱恩·拉斯穆森(Brian Rasmussen)(见接受的答案),问题已经被确定为由字串实习生表造成的LOH的碎片! 我写了一个快速testing应用程序来确认这一点:
static void Main() { const int ITERATIONS = 100000; for (int index = 0; index < ITERATIONS; ++index) { string str = "NonInterned" + index; Console.Out.WriteLine(str); } Console.Out.WriteLine("Continue."); Console.In.ReadLine(); for (int index = 0; index < ITERATIONS; ++index) { string str = string.Intern("Interned" + index); Console.Out.WriteLine(str); } Console.Out.WriteLine("Continue?"); Console.In.ReadLine(); }
应用程序首先创build并取消引用循环中的唯一string。 这只是为了certificate在这种情况下内存不会泄漏。 显然它不应该也不会。
在第二个循环中,创build并实现唯一的string。 这个行为根源于实习生表。 我没有意识到的是实习生表是如何performance的。 它看起来是由在LOH中创build的一组页面(128个string元素的对象数组)组成的。 这在CDB / SOS更为明显:
0:000> .loadby sos mscorwks 0:000> !EEHeap -gc Number of GC Heaps: 1 generation 0 starts at 0x00f7a9b0 generation 1 starts at 0x00e79c3c generation 2 starts at 0x00b21000 ephemeral segment allocation context: none segment begin allocated size 00b20000 00b21000 010029bc 0x004e19bc(5118396) Large object heap starts at 0x01b21000 segment begin allocated size 01b20000 01b21000 01b8ade0 0x00069de0(433632) Total Size 0x54b79c(5552028) ------------------------------ GC Heap Size 0x54b79c(5552028)
呃LOH段的转储揭示了我在泄漏的应用程序中看到的模式:
0:000> !DumpHeap 01b21000 01b8ade0 ... 01b8a120 793040bc 528 01b8a330 00175e88 16 Free 01b8a340 793040bc 528 01b8a550 00175e88 16 Free 01b8a560 793040bc 528 01b8a770 00175e88 16 Free 01b8a780 793040bc 528 01b8a990 00175e88 16 Free 01b8a9a0 793040bc 528 01b8abb0 00175e88 16 Free 01b8abc0 793040bc 528 01b8add0 00175e88 16 Free total 1568 objects Statistics: MT Count TotalSize Class Name 00175e88 784 12544 Free 793040bc 784 421088 System.Object[] Total 1568 objects
请注意,对象数组的大小是528(而不是1056),因为我的工作站是32位,应用程序服务器是64位。 对象数组仍然是128个元素。
所以这个故事的道德是要非常小心的实习。 如果您正在实习的string不知道是有限集的成员,则至less在CLR的第2版中,您的应用程序将由于LOH的碎片而泄漏。
在我们的应用程序的情况下,反序列化代码path中有一些通用代码,用于在反编组期间实习实体标识符:我现在强烈怀疑这是罪魁祸首。 然而,开发人员的意图显然是好的,因为他们想确保如果同一个实体被多次反序列化,那么只有一个标识符string的实例将被保留在内存中。
CLR使用LOH预分配一些对象(例如用于internedstring的数组 )。 其中一些小于85000字节,因此通常不会在LOH上分配。
这是一个实现的细节,但是我认为这是为了避免不必要的垃圾收集,只要程序本身就可以存活。
此外,由于有一些比较深奥的优化,在LOH上也会分配1000个或更多的元素。
为什么微软不能在.NET中为我们提供一个简单的API:
GC.CompactLargeObjectHeap()
即使这种方法导致应用程序挂起10秒或更多 – 它击败了崩溃,并要求应用程序重新启动清理内存!
有很多应用程序使用第三方库来分配LOH。 例如,即使是微软自己的XNA,在你每次做“新的Texture2D()”或者“Texture.FromFile()”的时候,都会将LOH碎片化。
所以,如果你想在.NET中创build一个“游戏引擎” – 这是一个真正的痛苦,让我们的LOH泄漏内存,没有任何方法阻止它。 我们所能做的只是“放慢速度”,除非我们决定“build立自己的图书馆”而忘记XNA。 这当然不是微软想要的!
如果MS只是给我们提供这一种方法,我们(和其他人)将解决许多个头痛的问题; 我会再说一遍:
GC.CompactLargeObjectHeap()
而当我提出这个build议时,我会build议MS进一步,并允许这个命令:
GC.DoLargeObjectHeapCompactionWhenOutOfMemory = true; // would be false by default
所以我们只需将“Visual3D游戏引擎”(我们的产品)设置为true,然后自动地,当碎片引起第一个OOMexception时,.NET会自动压缩LOH,然后重试分配!
哇,现在这将符合所有的.NET的炒作“它只是工作”和“易于使用”…只会导致这种“性能打击”的极端情况下,替代是OOM崩溃,迫使重新启动的应用程序!
请微软 – 为我们做这个!
请不要公开你的无知,只是说“只是使用更好的内存pipe理”的陈腐的天真的build议 – yeesh! 这是非常不恰当的build议! 你也必须说“写所有你自己的第三方库”(如XNA)!
为什么微软已经没有在.NET中提供这种行为? 会有什么伤害? 这将使我们从这些非常令人沮丧,费时的创可贴解决scheme中解脱出来,这些解决scheme最多可以“减缓内存泄漏”,但从未完全解决这些问题。
我们要求的解决scheme对那些不想使用它的人来说是无害的,并为那些真正想要使用它的人提供了一个令人耳目一新的救恩。 请,请,请在.NET 4.0 +中提供此。
.NET Framework 4.5.1能够在垃圾收集期间显式压缩大型对象堆(LOH)。
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect();
在GCSettings.LargeObjectHeapCompactionMode中查看更多信息
在阅读关于GC如何工作的描述时,以及关于第二代多长时间的物体的部分,而LOH物体的收集只发生在全集合 – 就像第二代的收集一样,这个想法是想到的。为什么不把第二代和大型物体放在同一堆呢,因为它们将被收集在一起?
如果这真的发生了,那么它就可以解释小小的物体是如何在与蕙兰相同的地方结束的 – 如果他们长期居住在第二代的话。
所以你的问题似乎是对我这个想法的一个相当好的反驳 – 这会导致LOH的分裂。
总结:你的问题可以由LOH和第二代共享相同的堆区来解释,尽pipe这并不能certificate这是解释。
更新: !dumpheap -stat
的输出几乎把这个理论从水里吹出来了! 第二代和蕙有自己的地区。
如果格式是可识别的应用程序,为什么你没有识别生成这种string格式的代码? 如果有几种可能性,请尝试添加唯一的数据来确定哪个代码path是罪魁祸首。
事实上,arrays与大量的自由物品交错,使我猜测他们最初是配对的,或者至less是相关的。 尝试识别释放的对象以找出生成它们的内容和相关的string。
一旦你确定什么是产生这些string,试图找出什么会阻止他们被GC。 也许他们被塞进一个被遗忘或未被使用的列表中,用于logging目的或类似的东西。
编辑:忽略内存区域和特定的数组大小的时刻:只是找出正在做什么这些string造成泄漏。 当您的程序创build或操作这些string一两次,当有更less的对象追踪时,请尝试!GCRoot。
伟大的问题,我通过阅读的问题了解到。
我认为反序列化代码path的其他位也使用大对象堆,因此碎片。 如果所有的string都在同一时间被实行,我认为你会没事的。
鉴于.net垃圾回收器是多么好,只是让反序列化代码path创build正常的string对象可能是足够好的。 直到需求被证实,不要做任何更复杂的事情。
我最多只会看看你看到的最后几个string的哈希表,并重复使用它们。 通过限制哈希表大小并在创build表时传递大小,可以阻止大多数碎片。 然后,您需要一种方法来删除最近从哈希表中未看到的string,以限制其大小。 但是,如果反序列化代码path创build的string无论如何都是短暂的,那么如果有的话,您将不会获得太多的收益。
这里有几个方法来识别LOH分配的确切的调用栈 。
并且为了避免LOH碎片预先分配大量的对象并将其固定。 需要时重新使用这些对象。 这里是LOH碎片的post 。 像这样的东西可以帮助避免LOH碎片。
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GC.Collect();