上图中第一次垃圾回收之前有四个对象obj0-3;在第一垃圾回收之后obj1和obj3被回收了,同时obj2和obj0移动到一起了;在第二次 垃圾回收之前有分配了三个对象obj4-6;在第二次执行垃圾回收之后obj2和obj5被回收了,obj4和obj6被移动到obj0旁边。
下图是大对象堆LOH回收示意图
可以看到在未执行垃圾回收之前,一共有四个对象obj0-3;第一次二代垃圾回收之后obj1和obj2被回收掉了,回收掉之后obj1和obj2 所占空间被合并到了一起,在obj4申请分配内存时就把obj1和obj2回收后释放的空间分配给它了;同时留下了一块内存碎片。如果这个碎片的大小小于 85000byte,那么这个碎片就在这个程序的生命周期中永远不能被再次利用了。
如果大对象堆上没有足够的空闲内存容纳要申请的大对象空间,CLR首先会尝试向操作系统申请内存,如果申请失败,就会触发一次二代回收来尝试释放一些内存。
在2代垃圾回收时,可以将不需要的内存通过VirtualFree交还给操作系统。交还的过程参见下图:
什么时候回收大对象呢?
在讨论什么时候回收大对象之前先来看下普通的垃圾回收操作什么时机执行吧。垃圾回收在下列情况下发生:
1. 申请的空间超过0代内存大小或者大对象堆的阀值,多数的托管堆垃圾回收在这种情况下发生
2. 在程序代码中调用GC.Collect方法时;如果在调用GC.Collect方法是传入GC.MaxGeneration参数时,会执行所有代对象的垃圾回收,包括大对象堆的垃圾回收
3. 操作系统内存不足时,当应用程序收到操作系统发出的高内存通知时
4. 如果垃圾回收算法认为做二代回收是有收效时会触发二代垃圾回收
5. 每一代对象堆的都有一个所占空间大小阀值的属性,当你分配对象到某一代,你增长了内存总量接近了该代的阀值,或者分配对象导致这一代的堆大小超过了堆阀 值,就会发生一次垃圾回收。因此当你分配小对象或者大对象时,会对应消耗0代堆或者大对象堆的阀值。当垃圾回收器将对象代数提升到1代或者2代时,会消耗 1、2代的阀值。在程序运行中这些阀值是动态变化的。
大对象堆性能影响
让我们先看下分配大对象的代价。 CLR为每个新对象分配内存时都要保证这些内存清空的,是没有被其他对象使用的(I give out is cleared)。这就意味着分配的代价完全被清理(clearing)的代价控制着(除非在分配时触发了一次垃圾回收)。如果清空1byte需要2个周 期(cycles),就意味着清除一个最小的大对象需要170,000个周期。通常情况下人们不会分配超大的对象,比如说在2GHz的机器上分配16M大 小的对象,大约需要16ms来清空内存。这代价太大了。
让我们在看下回收的代价。前面提到过,大对象和2代龄对象一起回收。如果大对象或者2代对象占用空间超过其阀值时,就会触发2代对象的回收。如果2 代回收因为大对象堆超过阀值被触发,2代对象堆本身没有多少对象可以做回收。如果在2代堆上没有多少对象,这问题不大。但是如果2代堆很大对象很多,过多 的2代回收就会导致性能问题。如果是临时性的分配大对象,就需要很多的时间来运行垃圾回收;也就是说如果你持续的使用大对象然后又释放大对象对性能会有很 大的负面影响。
大对象堆上的巨大对象通常是数组(很少有一个对象很大的情况)。如果对象中的元素是强引用,代价会很高;如果元素之间没有相互引用,垃圾回收时就不需要遍历整个数组。例如:用一个数组来保存二叉树的节点,一种方法是在节点中强引用左右节点:
class Node
{
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/fuwuqi/)Data d;
Node left;
Node right;
}
Node[] binaryTree = new Node[num_nodes];
如果num_nodes是一个很大的数字,就意味着每个节点都至少需要查看二个引用元素。一种替代方案是在节点中保存左右节点元素的数组索引号
class Node
{
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/fuwuqi/)Data d;
uint left_index;
uint right_index;
}
这样的话,元素之间的引用关系去掉了;可以通过binaryTree[left_index]来获得引用的节点。垃圾回收器在做垃圾回收时也不需要看相关的引用元素了。
为大对象堆收集性能数据
有几种方法可以收集大对象堆相关的性能数据。在我解释这些方法之前,让我们先谈一下为什么需要收集大对象堆相关的性能数据。
在你开始上搜集某个方面的性能数据时,有可能你已经找到这方面造成性能瓶颈的证据;或者你已经没有找遍了所有方面都没有发现问题。
在查找性能问题时.Net CLR Memory 性能计数器通常是应该先考虑使用的工具。和LOH相关的计数器有generation 2 collectioins(2代堆收集次数)和large object heap size大对象堆大小。Generation 2 collections显示的是进程启动之后2代垃圾回收操作发生的次数。Large object heap size计数器显示的是当前大对象堆的大小值,包括空闲空间;这个计数器是在每次垃圾回收操作之后做更新,并非每次分配内存都做更新。
可以参考下图在windows性能计数器中观察.Net CLR Memory相关性能数据
你也可以通过程序查询这些计数器的值;很多人通过程序的方式收集性能计数器来帮助查找性能瓶颈。
当然也可以使用调试器winddbg观察大对象堆。
(本文来源于图老师网站,更多请访问http://m.tulaoshi.com/fuwuqi/)最后提示一下:到目前为止,大对象堆作为垃圾回收的一部分是不做内存碎片整理的,但是这个只是一个clr的实现细节,程序代码不应该依赖这个特点。如果要确保对象不会被垃圾回收器移动,就要使用fixed语句。
文章来源:伯乐在线