标签:
作者: Maoni Stephens (@maoni0) - 2015
附: 关于垃圾回收的信息,可以参照本文末尾资源章节里引用的垃圾回收手册一书。
GC包含的两个组件分别是内存分配器和垃圾收集器。内存分配器负责获取更多的内存并在适当的时候触发垃圾收集。垃圾收集器回收程序中不再使用的对象的内存。
有多种方法调用垃圾回收器,例如人工调用GC.Collect或者当终结线程在接收到表示低内存的异步通知时(调用)。
内存分配器由执行引擎(EE)的内存分配辅助函数调用,并附上下列信息:
GC不会区别对待不同的对象。请通过执行引擎来获取对象的大小。
基于对象的大小,GC将其分成两类:小对象(< 85,000字节)和大对象(>= 85,000字节)。原则上,大小对象都可以同样处理,但是压缩大对象耗费更加昂贵所以GC才这样区分。
GC向内存分配器释放内存是通过内存分配上下文完成的。内存上下文的大小有分配额度定义:
大对象不使用分配上下文和定额。一个大对象本身就比这些小内存区域(8k的定额)大了。而且,这些区域的优点(下文讨论)直适用于小对象。大对象就直接在堆区上分配了。
分配器的设计目标如下:
Object* GCHeap::Alloc(size_t size, DWORD flags);
Object* GCHeap::Alloc(alloc_context* acontext, size_t size, DWORD flags);
上面的函数可以用来分配大对象和小对象。也有一个对象可以直接在大对象堆里分配内存:
Object* GCHeap::AllocLHeap(size_t size, DWORD flags);
GC将极其高效利用内存和尽量避免编写“托管代码”的程序员的人工干预作为奋斗目标。高效是指:
CLR GC是一个分代收集器,即对象是逻辑划分成几个代的。当第 N 代收集完毕后,剩下来的存活对象则被标识为第 N+1 代。这个过程被称作升级。也有异常情况我们决定降级或者不升级。
小对象堆被分成3代:gen0, gen1和gen2。大对象只有一代 - gen3。gen0和gen1被称为短命代(对象存活的时间不长)。
对于小对象堆,代的数字表示它的年龄 - gen0属于最年轻的一代。这不是说gen0里所有的对象比gen1或gen2中任意一个对象年轻。后文会提到一些异常情形。收集一代是指收集这一代和所有比其年轻的代。
原则上大对象可以使用跟小对象相同的办法处理,但是压缩大对象的代价很高,才区别对待。出于性能的考量,大对象只有一代而且总是跟gen2一起收集。gen2和gen3可以很大,但是收集短命代(gen0和gen1)的成本有限制。
内存分配是在最年轻的代发生的 - 对小对象来说总是gen0,而对大对象来说是gen3,因为只有一代。
托管堆是一系列的托管堆区。一个托管堆区是GC从操作系统那里申请的一个连续的内存区域。堆区被分成大小对象区,对应大小对象。每个堆的堆区都链在一起。至少有一个小对象堆区和一个大对象堆区 - 用来为加载CLR而保留。
每个小对象堆总是只有一个短命区,用来保存gen0和gen1代。这个堆区有可能包含gen2的对象。除了短命区以外,有可能有零个、一个或多个额外的堆区,用来作为gen2堆区并保存gen2对象。
在大对象堆上有一个或多个堆区。
堆区的使用是从低地址开始到高地址,即堆区里低地址对象的时间比高地址对象久。同样下文也有一些异常情况。
堆区可以按需申请,如果其不包含存活对象就会被删除,但是堆上初始的第一个堆区一直都在。对于每个堆,一次申请一个堆区,这个在给小对象做垃圾回收时和创建大对象时发生。这样做有更好的性能,因为大对象只会跟gen2一起回收(执行起来代价更高)。
堆区按照申请的顺序链接在一起。链表上最后一个堆区永远是短命区。回收过的堆区(没有存活对象)会被复用而不是直接被删除,也就是变成新的短命区。堆区复用只发生在小对象堆。每当分配一个大对象,会考虑整个大对象堆。而小对象的分配只考虑短命区。
分配预算是跟每个代关联的逻辑概念。这是代里的一个大小限制用来在超出时触发一个GC。
预算是设置在代上基于该代对象存活率的一个属性。如果存活率高,那么预算就会大一些,这样在下一次GC的时候销毁的对象和存活的对象有一个更好的比率。
当触发一个GC时,GC必须决定回收哪一代。除了分配预算以外还要考虑以下几个因素:
标注阶段的目标是找出所有存活的对象。
按代回收的好处是只需要考虑堆的一部分而不是每次都处理所有对象。当回收短命代时,GC只需要找到这一个代里存活的对象,这些信息由执行引擎上报。除了执行引擎可能引用对象以外,更老一代的对象也可能会引用新一代的对象。
对于GC使用卡片来标注更老的代。卡片是由JIT辅助函数在分配操作时设置的。如果JIT辅助函数看到一个对象在短命区的范围,然后设置包含卡片的字节来指示其来源位置。在收集短命区时,GC可以在看堆上设置过的卡片并依次处理卡片对应的对象即可。
计划阶段模拟压缩过程来决定最后的效果,如果压缩效果很好那么GC就会启动压缩,否则执行清理。
如果GC决定压缩,其结果会移动对象,那么对这些对象的引用必须更新。迁移阶段需要处理所有指向所回收的代中的对象的引用。相比之下,而标注阶段只处理存活对象因此不需要考虑弱引用(weak reference)。
这个阶段很直观,因为在计划阶段就已经计算对象应该移动的新地址,压缩阶段只需要将对象拷贝过去。
清理阶段会查看两个存活对象之间的空间。其为这些空间创建闲置对象。相邻的闲置对象会合并。它会将所有的闲置对象保存在 闲置对象列表(freelist)。
术语:
这些说明了一个后台GC是如何实施的:
这个场景跟WKS GC并打开了并行GC一样,除了在服务器GC线程上没有后台GC。
这个章节用来帮助你理解代码过程。
用户线程用完定额之后,通过try_allocate_more_space申请新定额。
try_allocate_more_space在需要触发GC时调用GarbageCollectGeneration。
假如WKS GC并关闭了并行GC,GarbageCollectGeneration在触发GC的用户线程上执行,代码过程如下:
GarbageCollectGeneration()
{
SuspendEE();
garbage_collect();
RestartEE();
}
garbage_collect()
{
generation_to_condemn();
gc1();
}
gc1()
{
mark_phase();
plan_phase();
}
plan_phase()
{
// actual plan phase work to decide to
// compact or not
if (compact)
{
relocate_phase();
compact_phase();
}
else
make_free_lists();
}
假如WKS GC并打开了并行GC(默认情况),后台GC的代码过程如下:
GarbageCollectGeneration()
{
SuspendEE();
garbage_collect();
RestartEE();
}
garbage_collect()
{
generation_to_condemn();
// decide to do a background GC
// wake up the background GC thread to do the work
do_background_gc();
}
do_background_gc()
{
init_background_gc();
start_c_gc ();
//wait until restarted by the BGC.
wait_to_proceed();
}
bgc_thread_function()
{
while (1)
{
// wait on an event
// wake up
gc1();
}
}
gc1()
{
background_mark_phase();
background_sweep();
}
标签:
原文地址:http://www.cnblogs.com/killmyday/p/5493053.html