【IT168 评论】了解垃圾回收机制对使用.Net很重要。CLR管理两个独立的堆,即小对象堆(SOH)和大对象堆(LOH)。本文将重点介绍运行时如何管理大对象堆、大对象堆产生内存碎片的原因以及大对象堆(LOH)内存碎片最小化的方法。
小对象堆垃圾回收机制
垃圾回收器会适当清理不使用的应用程序。小对象堆和大对象堆的在垃圾回收器中工作方式不同,我们先来了解一下小对象堆的工作方式。
当创建的对象小于85K时存储在小对象堆中。CLR将小对象堆依据不同时间间隔收集分成三代——第0代、第1代和第2代。小对象通常被分配给第0代,如果它们在GC周期中存活,则被提升为第1代。如果在下一次GC周期中依然存活,就会被提升到第2代。
小对象堆垃圾收集采用压缩的方法,这意味着当收集到未使用的对象时,GC将活动对象移动到间隙、消除碎片,以确保可用内存连续。当然,压缩包括开销——两个CPU周期和用于复制对象的附加内存。压缩会在小对象堆上自动执行,这样就大大降低小对象的成本。
大对象堆中内存碎片化的形成
但以上压缩方法由于成本太高,不适用于大对象堆(大于85K)。除此之外,复制和移动大型对象会涉及到垃圾收集器巨大开销、GC需要的内存是垃圾收集的两倍、再次移动大对象也会非常耗费时间也是不适用于大对象堆的主要原因。所以大对象堆的垃圾收集不采用压缩的方法。那大型对象堆中的内存如何回收?
在大对象堆中,GC从不移动大对象,只需在不需要时删除。在不断删除存储的过程中,大对象堆中逐步存在内存漏洞,这就是所谓的内存碎片化。
虽然GC不对大对象堆进行压缩,但是会将其中的相邻空闲块连在一起,这样会创造一个更大的空闲块,并将其作为优化策略添加到空闲列表中。
需要注意的是,GC仅在第2代中从大对象堆中收集未使用的对象。换句话说,在从大对象堆中回收内存之前,GC会先回收驻留在小对象堆中的内存。因此,大对象堆不仅受到内存碎片的影响,而且具有存活时间更长、在不被使用情况下也占用空间的特性。
在这个过程中,压缩对象的成本与大小成正比,而且这个成本不低,还会降低性能,所以大对象就被存储在一个单独的堆中。
大对象堆保持最小碎片化实践
上面我们已经了解到大对象堆碎片化产生的原因,下面我们来介绍保持大对象最小碎片化的做法。
推荐的做法是识别应用程序中的大对象,然后将其分割成较小的对象——也可能会使用一些包装类(wrapper class)。另一种是重新设计应用程序,在设计过程中避免大对象的使用。还可以定期回收应用程序池。
虽然GC本身不会压缩大对象堆,但我们可以采用代码对大对象堆进行压缩。以下代码片给出实现过程:
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
其实在.Net Framework 4.5中管理大对象堆的方式有了许多显着改进。
运行时通过反复检查使用可用内存块管理大对象堆。在.Net框架的先前版本中,一个内存块一旦被拒绝作为分配的候选对象,在后续分配中将不再考虑。
第二个改进是,如果你使用的是Server GC模式,运行时均衡应用程序使用中所有逻辑处理器之间的大对象堆分配,在.NET Framework 4.5之前,只有小对象堆分配在整个处理器之间进行了平衡。这些都较高提升了大对象堆内存性能。
总而言之,垃圾收集器运行时将小对象堆作为优化策略的一部分来消除大对象堆内存漏洞,但是永远不会因为性能对大型对象堆进行压缩处理。但如果在x86系统中运行使用许多大对象的程序,可能会遇到OutOfMemory异常,如果在x64系统中运行,可能会有碎片化堆的生成。通过了解垃圾收集机制和大对象堆的复杂性,我们可以采用避免内存碎片化的策略帮助应用程序正常运行。