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