.Net7 GC标记阶段代码的改变

前言

在阅读.Net7的CLR时候,发现了一个不通的地方。也就是通过GCInfo获取到了对象之后,它并没有在GcScanRoots(对象扫描标记)里面对它进行标记,那么如果没有标记这个对象如何被计划阶段构建呢?仔细研读,发现它跟之前的代码之所以不同,是因为它把标记抽取出来,另外形成一个数组循环标记。本篇来看下。.

概括

1.问题:
假如说有以下示例代码:

static void Main(string[] args){    Console.WriteLine("Tian Xia Feng Yun Chu Wo Bei!\r\n");    Program PM= new Program();    PM = null;    GC.Collect();}

调用GC.Collect()函数,GC垃圾回收的第一步,就是标记。这个标记实质上可以分为以下几步。

一:获取到所有的线程(GetAllThreadList)
二:遍历循环这些线程的帧
三:通过遍历到的帧,找到这些帧对应的GCInfo
四:通过GCInfo的偏移量和寄存器找到相对应的对象
五:对找到的对象进行标记。

以上四步,基本上没变。第五步标记的时候,它加入了一些新的代码。

uint8_t *mark_queue_t::queue_mark(uint8_t *o){    Prefetch (o);    size_t slot_index = curr_slot_index; //这里有一个slot的索引    uint8_t* old_o = slot_table[slot_index];// 这里把这个索引的值从数组取出来    slot_table[slot_index] = o;//把新对象赋值到索引所在的数组内存    curr_slot_index = (slot_index + 1) % slot_count;    if (old_o == nullptr)//这个地方是关键,因为假如说你按照上面的示例代码,之前并没有这个PM对象。所以这个old_o是等于nullptr的,所以它直接return了。那么下面就不存在标记了。问题是这个标记不标记??还是在别的地方标记了??        return nullptr;    BOOL already_marked = marked (old_o);    if (already_marked)    {        return nullptr;    }    set_marked (old_o);    return old_o;}

二:解决

要解决这个问题,就需要知道数组slot_table里面的数值是何时被变动的。这个其实很简单在VS里面可以,打个条件断点--值更改时中断。
结果发现在函数get_next_marked里面对slot_table数组里面的值(也就是对象)进行了一个标记

uint8_t* mark_queue_t::get_next_marked(){    size_t slot_index = curr_slot_index; //获取到当前slot_table数组的总数索引    size_t empty_slot_count = 0;     while (empty_slot_count < slot_count) //从零开始循环总数索引    {        uint8_t* o = slot_table[slot_index]; //一个个的取出来,保存到o变量。        slot_table[slot_index] = nullptr; //然后把相应的索引位内存置0,以便下次标记的时候继续使用。        slot_index = (slot_index + 1) % slot_count;        if (o != nullptr) //如果这个o不等于null,那么下面的代码就是对它进行一个标记。        {            BOOL already_marked = marked (o); //判断它是否被标记            if (!already_marked) // 如果没有            {                 set_marked (o); // 则对它进行标记                curr_slot_index = slot_index;                 return o;//把标记过的对象返回            }        }        empty_slot_count++;//继续循环下一次,继续标记下一个    }    return nullptr;// 如果索引为空,则直接返回null}

这个函数是被drain_mark_queue函数调用,而前者则是在GCScanRoot整个函数被完成之后调用的。那么整体的就打通关节了,实质上它是抽取出来了,重新进行了标记。而非在GCScanRoot里面进行标记。