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

发布时间 2023-03-22 19:15:29作者: 江湖评谈

前言

由于业务需求,在探究.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里面进行标记。


结尾:

作者:江湖评谈。公众号:jianghupt。扫码关注我,带你了解高阶技术,不局限于.Net。
image