.Net7 GC標記階段程式碼的改變

江湖評談發表於2023-03-17

前言

由於業務需求,在探究.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

相關文章