D 程式設計/垃圾收集器/關於更好的 GC 實現的想法
應該能夠擁有不同的 GC 實現。例如
- 精確
- 併發
- 分代
- 壓縮/移動
這導致了更具體的需求
一個精確的 GC 應該只標記那些從指標引用的記憶體。它不應該標記從非指標型別引用的記憶體。移動 GC 還需要修改指標值,如果一個物件被重新定位。
如果掃描一個指標,應該知道物件的起始地址。因此,應該可以獲取 PLI(指標位置資訊),這是繼續進行的必要條件。
在併發/增量 GC 實現中,移動引用是一個問題。
Object refa = new Object; Object refb = null;
- 開始進行垃圾收集週期
- gc 掃描 refb,它為空
- gc 暫停或被另一個執行緒中斷
- 在其他地方:refb = refa; refa = null;
- gc 繼續(認為 refb 為空)
- gc 掃描 refa,它現在也為空
- GC:哦,沒有指向 obj 的 ref,我可以釋放它!
- 應用程式嘗試引用 refb,它不為空,並崩潰
這就是需要寫屏障的原因。在併發/增量 GC 中,每次寫入引用都需要使舊目標引用值的掃描失效。
由於精確掃描堆疊可能是一個難題(尤其是在考慮 C 介面時,例如從 C 程式碼到 D 程式碼的回撥),可以簡單地保守地掃描堆疊並“固定”所有可能從堆疊引用的物件。新的Mono GC就是這麼做的。
參見“保守掃描”部分。堆疊的處理方式類似於當前的標記和清除演算法。
為了支援
Object.toHash()
在移動 GC 存在的情況下,需要將雜湊碼儲存在物件本身中。雜湊碼可以從物件的地址獲取,但由於它的地址可能發生變化,因此需要儲存它。
應該考慮支援弱指標,它們是可能有用且經常被請求的功能。如果不再有指向物件的非弱指標,弱指標將被設定為 null。Java 和 C# 也支援弱指標。
語言應該允許指向原生型別,但它們不應該與 GC 相關。這意味著,它們不應該阻止 GC 刪除物件,並且它們不被標記為“指標”。
因此,GC 不需要查詢物件的起始位置。程式設計師只需要確保他不會對成員進行引用,這些成員的壽命比整個物件更長。
如果可以在物件內部儲存有限數量的 GC 資料(例如標誌),那麼特定的 GC 實現可能會更有效地實現。但請注意,大多數管理資料可以儲存在包含該物件的記憶體塊中。
頭腦風暴可能的具體物件佈局(僅針對 GC)
前 4 個位元組將包含一個型別資訊指標,型別資訊包含物件的佈局資訊。這種佈局資訊將以點陣圖或區域列表((偏移量,長度) 對的列表)的形式存在。
一個非常專門的想法是將指標資料和整數資料分離,因此 GC 掃描物件不需要點陣圖或類似的複雜資料結構。指標和整數將被完全分離
+----------+
+ integers +
. ... .
+----------+
+ reflen +
+----------+ <------ object ptr
. ... .
+ pointers +
+----------+
指向物件的指標始終指向物件的中間。在物件指標之上,所有資料都是整型資料;在物件指標之下,所有資料都是指標資料。GC 可以使用 “reflen” 成員來找到需要掃描的區域的大小。
繼承類可以將它們的整型/指標資料附加到基地址的資料之上/之下。如果物件被構造,則需要正確設定 “reflen” 成員。
雖然這將非常快(與點陣圖相比),但它也可能非常難以整合到現有的編譯器/環境/工具鏈中。使用內部指標,類頭將更難找到。結構體陣列也將是一個(可能可以解決的)問題。
也可以將指標資料組織成區域,並像單鏈表一樣連結這些指標資料區域(例如在存在繼承的情況下,指標不能分組到單個區域)。每個指標資料區域都以包含區域大小的非指標字開始,並且物件的第一個字包含指向第一個指標資料區域的指標,或者物件本身以該區域開始。
一些 GC 需要在每次覆蓋引用或讀取引用時執行一些程式碼。一種靈活的方式是,為編譯器提供一個函式,用於讀取和寫入訪問。這些函式應該始終被內聯和最佳化。例如:
___ref_assign( void * trg, void * src ){ trg = src; }
void* ___ref_read( void * src ){ return src; }
GC 的分配函式不僅應該接收所需的記憶體大小,還應該接收一個位域,其中包含該記憶體中哪些字是引用的資訊。
每個棧幀都以包含引用資訊的位域開始。如果 GC 掃描棧,則需要識別這些幀。
編譯器應該確保,引用值始終也存在於記憶體中。這樣 GC 就無需掃描暫存器,也不需要知道哪個暫存器包含指標值。
針對併發實現。應該支援遞迴使用。這意味著它應該與計數器一起使用。計數器在每次呼叫 disable 時遞增,在每次呼叫 enable 時遞減。
針對移動/壓縮型 GC 實現。
強制執行收集週期。呼叫將在收集週期結束後返回。在 Stop-the-world 實現中,這意味著所有執行緒將暫停。在併發實現中則不會。引用計數解決方案將立即返回。