.NET Core GC压缩(compact_phase)底层原理浅谈

简介

终于来到了GC的最后一个步骤,在此之间,大量预备工作已经完成。万事俱备,只欠东风

清除

如果GC决定不压缩,它将仅执行清除操作。清除操作非常简单,把所有不可到达对象(gap),转换成Free。也就是转换成空闲内存空间。

由于所有的繁重计算任务在plan_phase阶段均已完成,所以步骤比较简单

  1. 基于gap的size创建空闲列表

    free > 2 * min_obj_size 的Free块会被放入空闲列表,小于此大小的不再被利用,但会纳入内存碎片统计

  2. 恢复"被销毁"的前置plug和plug

    这是pinned 对象的特殊情况,pinned的plug前面可能还是一个plug,所以没有gap来存放, 因此会根据实际情况"钉住"它的前面或者后面的Plug.来暂存gap_reloc_pair信息。 所以用完后还要"还回去"

  3. 更新终结队列,并提升或降低plug的代

  4. 更新段空间

眼见为实

压缩

如果GC决定压缩,就比较复杂了。总体分为两步

  1. 复制对象并移动到新位置(重定位阶段)
  2. 将新对象的地址在root上更新

GC重定位阶段

此步骤更新所有对稍后要移动对象的引用,为了更新这些地址,要扫描他们的root,并逐一更新

  1. 栈空间的root
  2. 跨代记忆集的root
  3. 托管堆中的root
  4. 前置plug与后置Plug的root
  5. 终结器队列的root
  6. 句柄表的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要移动所有对象。由以下几个步骤组成

  1. 复制对象
  2. 恢复"被销毁"的前置plug和plug
  3. 重新划分代边界
  4. 释放内存段
  5. 创建空闲列表

眼见为实

GC前:

GC后:对象被移动,原有地址被压缩释放

眼见为实:复制连续的内存区域

以滑动的方式来copy内存,避免出现覆盖问题