简介
终于来到了GC的最后一个步骤,在此之间,大量预备工作已经完成。万事俱备,只欠东风
清除
如果GC决定不压缩,它将仅执行清除操作。清除操作非常简单,把所有不可到达对象(gap),转换成Free。也就是转换成空闲内存空间。
由于所有的繁重计算任务在plan_phase阶段均已完成,所以步骤比较简单
-
基于gap的size创建空闲列表
free > 2 * min_obj_size 的Free块会被放入空闲列表,小于此大小的不再被利用,但会纳入内存碎片统计
-
恢复"被销毁"的前置plug和plug
这是pinned 对象的特殊情况,pinned的plug前面可能还是一个plug,所以没有gap来存放, 因此会根据实际情况"钉住"它的前面或者后面的Plug.来暂存gap_reloc_pair信息。 所以用完后还要"还回去"
-
更新终结队列,并提升或降低plug的代
-
更新段空间
眼见为实
压缩
如果GC决定压缩,就比较复杂了。总体分为两步
- 复制对象并移动到新位置(重定位阶段)
- 将新对象的地址在root上更新
GC重定位阶段
此步骤更新所有对稍后要移动对象的引用,为了更新这些地址,要扫描他们的root,并逐一更新
- 栈空间的root
- 跨代记忆集的root
- 托管堆中的root
- 前置plug与后置Plug的root
- 终结器队列的root
- 句柄表的root
比如某个对象的内存地址为0x1000,压缩后它的新地址为0x500。那就就要对该对象的所有root更新内存地址。
眼见为实
点击查看代码
internal class Program
{
static void Main(string[] args)
{
Append();
AppendStatic();
Compact();
}
public static Person person;
public static List<byte[]> list = new List<byte[]>();
static void Append()
{
//填 10M 数组到 临时段上
for (int i = 0; i < 1024 * 10; i++)
{
list.Add(new byte[1000]);
}
Console.WriteLine("1. 10M 数据已分配完毕,请查看临时段大小,准备分配 Person 对象!");
Debugger.Break();
}
static void AppendStatic()
{
person = new Person();
list = null;
Console.WriteLine("2. Person 已分配,list已去根,请再次观察托管堆!准备触发 GC,请下 compact_phase 断点!");
Debugger.Break();
}
static void Compact()
{
GC.Collect(2, GCCollectionMode.Forced, true, true);
Console.WriteLine("3. GC 已触发,请观察 Person 是否已变!");
Debugger.Break();
}
}
public class Person { }
在bp coreclr!WKS::gc_heap::compact_phase 下断点,观察对象的新老地址变化
GC前:
GC后:内存地址发生变化
眼见为实
压缩对象
在上面更新root的操作完成后,GC要移动所有对象。由以下几个步骤组成
- 复制对象
- 恢复"被销毁"的前置plug和plug
- 重新划分代边界
- 释放内存段
- 创建空闲列表
眼见为实
GC前:
GC后:对象被移动,原有地址被压缩释放
眼见为实:复制连续的内存区域
以滑动的方式来copy内存,避免出现覆盖问题