此文档来源于CoreCLR的BOTR(The Book of the Runtime), 点击打开原文
一切著作权归微软公司所有
作者: Maoni Stephens (@maoni0) - 2015
提示: 推荐看 The Garbage Collection Handbook 这本书学习更多关于GC的知识 (在文章底部的链接中)
在GC中有两个主要的组件, 一个是分配器(Allocator), 另一个是收集器(Collector).
分配器负责获取更多的内存并且在适当的时机触发收集器.
收集器负责回收垃圾和不再被程序使用的对象内存.
此外还有一些途径可以触发收集器, 例如手动调用GC.Collect函数或析构线程(Finalizer Thread)收到一个内存不足的异步通知(由收集器发送).
分配器由运行引擎(Execution Engine (EE))调用, 调用时会带有以下的信息:
GC不会根据对象类型的不同做出特殊处理, 它会从运行引擎获取对象的大小, 根据对象的大小把对象分为两类:
原则上小对象和大对象都可以用同样的方式处理, 但因为压缩(Compacting)大对象的代价会比较昂贵所以GC会区分对待.
当GC把一段内存交给分配器时, 它会参照分配上下文(Allocation Context).
分配上下文的大小取决于分配单位(Allocation Quantum)的定义.
大对象不会使用分配上下文和分配单位, 因为一个大对象本身可以大于这些小区域.
并且使用这些区域带来的好处(会在下面讨论)仅仅受限于小对象.
大对象会直接在堆段(Heap Segment)中分配.
分配器的设计要求实现:
Object* GCHeap::Alloc(size_t size, DWORD flags);
Object* GCHeap::Alloc(alloc_context* acontext, size_t size, DWORD flags);
以上的函数可以用于分配小对象和大对象.
另外还有一个函数用于强制从大对象的堆(LOH: Large Object Heap)中分配内存:
Object* GCHeap::AllocLHeap(size_t size, DWORD flags);
GC致力于高效的内存管理, 只要求程序员付出很小的努力
高效指的是:
CLR GC把对象分成了不同的世代, 当第 N 世代的垃圾被回收后, 生存的对象会被标记为第 N+1 世代, 这个过程被称为升级(Promotion).
还有一些例外的情况当我们决定是否降级或不升级.
对于小对象的堆会分为3个世代: 第0世代(gen0), 第1世代(gen1)和第2世代(gen2).
对于大对象只有1个世代: 第3世代(gen3).
第0世代和第1世代被称为短暂(对象的生命周期短)的世代.
对于小对象的堆, 世代中的数字代表了年龄, 第0世代是最年轻的世代.
但不代表第0世代中的对象一定比第1世代和第2世代的对象年轻, 下面将会说明那些例外.
收集一个世代中的垃圾同时也会收集所有比它年轻的世代的垃圾.
原则上大对象可以使用和小对象一样的处理方式, 但是压缩大对象的代价会非常的昂贵, 因此大对象和小对象会受到不同的对待.
因为性能上的原因, 大对象只使用了一个世代(第3世代)并且这个世代会和第2世代一起回收垃圾.
因为第2世代和第3世代可以很大, 需要和短暂的世代(第0世代和第1世代)在开销上划出边界.
为对象分配内存时总会从最年轻的世代分配 - 对于小对象总会从第0世代分配, 对于大对象总会从第3世代分配(因为只有一个世代).
受管理的堆(Managed Heap)是一个包含了受管理的堆段(Managed Heap Segments)的集合.
受管理的堆段是从系统内核申请得到的一块连续的内存空间. (译者注: 即malloc/brk申请得到的空间)
用于区别小对象和大对象, 堆段又分为小对象堆段和大对象堆段.
每个堆中的堆段都是相互链接在一起的, 至少会有1个小对象堆段和1个大对象堆段 - 它们会在加载CLR时预留.
每个小对象的堆中只有一个短暂的堆段(Ephemeral Segment)用于存放短暂世代(第0世代和第1世代)的对象, 但也有可能包含第2世代的对象.
其他额外的堆段(0或1或更多个)中只会存放第2世代的对象.
每个大对象的堆中有一个或更多个堆段.
堆段中的空间会从较低的地址向较高的地址消耗, 即地址更小的对象比地址更大的对象更老. 这里也有一些例外将在下面说明.
堆段会在需要时向系统申请, 并且在不包含任何生存的对象时删除.
但是初始的堆段(加载时预留的)会一直保留.
每个堆中每次只会处理一段(而不是全部), 如在回收小对象和分配大对象时.
这样的设计提供了更好的性能, 因为大对象只会在第2世代回收时一同回收(代价相当昂贵).
堆段会按它们的申请顺序链接在一起, 链中的最后一个堆段一定是短暂的堆段(Ephemeral Segment).
回收的堆段(不包含任何生存的对象)不一定会被删除, 也可能被作为一个新的短暂的堆段, 这种机制仅在小对象的堆中实现.
每次分配大对象都会考虑整个大对象的堆, 而小对象仅仅考虑短暂的堆段.
分配预算是一个关联于每个世代的逻辑概念, 当世代的大小达到了指定的限制则会触发GC.
每个世代的预算值属性基本取决于该世代的对象的生存率, 如果生存率较高, 那么这个限制会调大使得下次对该世代的GC会得到一个更好的回收率.
当GC被触发时, GC首先需要确定回收哪个世代.
除了分配预算外, 还有这些因素需要考虑:
标记阶段的目标是寻找所有存活的对象.
多世代收集器的好处是每次只需要去看堆中最近的对象, 而不需要去看历史生成的所有对象.
当收集短暂世代(第0世代和第1世代)中的对象时, GC需要寻找这些世代中所有存活的对象,
运行引擎(EE)使用中的对象会标记为存活, 此外被其他对象(更老世代的对象)引用的对象也会标记为存活.
GC在标记更老的世代中的对象时会使用卡片(Cards),
JIT的帮助类会在赋值操作时设置卡片, 如果JIT的帮助类看到一个对象在短暂的范围中它会设置一个包含了源位置的卡片的字节.
在短暂世代的回收中, GC可以只看堆中其余的部分设置的卡片来找到它们对应的对象.
(译者注: 卡片表用于标记跨代引用,例如只扫代0的时候如果代1引用了代0的对象也要扫卡片表中代1的部分对象)
计划阶段会做一个比较来决定使用压缩(Compaction)还是清扫(Sweeps).
如果压缩效果更好则GC会开始实际的压缩, 否则GC会开始清扫.
如果GC决定要压缩, 那就要移动现有的对象, 指向这些对象的引用都需要被更新.
重定位阶段需要找出指向回收世代中的对象的所有引用.
相反, 标记阶段仅会参考存活的对象因此不需要考虑弱引用(Weak References).
这个阶段的目标非常直接, 因为计划阶段已经计算了对象应该移动到的新地址, 压缩阶段会复制对象到这些新地址中.
清扫阶段会寻找夹在存活对象中的空余空间(死对象), 并在这些空间里创建一些自由对象.
相邻的死对象会被合并成一个自由对象.
创建的自由对象会放到 自由对象列表(freelist) 里.
缩写: (译者注: 以下不会使用这些缩写)
这里说明了后台GC如何运作.
流程和工作站GC - 启用并发式GC一样, 除了非后台的GC也会在服务器GC线程中完成.
这一段旨在帮助你追踪代码流程.
当用户线程用完分配单位(Allocation Quantum), 需要一个新的分配单位时会调用try_allocate_more_space函数.
当try_allocate_more_space函数需要触发GC时会调用GarbageCollectGeneration函数.
在"工作站GC - 不启用并发式GC"的模式下, GarbageCollectGeneration会在触发GC的用户线程上完成所有工作, 代码流程是:
GarbageCollectGeneration()
{
SuspendEE();
garbage_collect();
RestartEE();
}
garbage_collect()
{
generation_to_condemn();
gc1();
}
gc1()
{
mark_phase();
plan_phase();
}
plan_phase()
{
// 实际的计划阶段, 判断要用压缩还是清扫
if (compact)
{
relocate_phase();
compact_phase();
}
else
make_free_lists();
}
在"工作站GC - 启用并发式GC"的模式(默认模式)下, 后台GC的代码流程是:
GarbageCollectGeneration()
{
SuspendEE();
garbage_collect();
RestartEE();
}
garbage_collect()
{
generation_to_condemn();
// 判断要用后台GC, 唤醒后台GC
do_background_gc();
}
do_background_gc()
{
init_background_gc();
start_c_gc ();
// 等待后台GC完成工作并重启受管理的线程
wait_to_proceed();
}
bgc_thread_function()
{
while (1)
{
// 等待事件
// 唤醒
gc1();
}
}
gc1()
{
background_mark_phase();
background_sweep();
}
这份文档简单的介绍了GC的设计和工作流程, 同时也带来和很多疑问, 例如一个程序有多少个堆和什么是卡片等
我将在CoreCLR源码探索的系列文章中分析CoreCLR的GC源码来解决这些疑问
另外博客园上已经有一位大神对CoreCLR的GC源码进行了部分分析,可以查看他的博客
原文:https://www.cnblogs.com/wl-blog/p/14535778.html