跳轉到內容

D 程式設計/垃圾收集器/關於更好的 GC 實現的想法

來自華夏公益教科書,開放書籍,構建開放世界

應該能夠擁有不同的 GC 實現。例如

  • 精確
  • 併發
  • 分代
  • 壓縮/移動

這導致了更具體的需求

哪個詞是指標

[編輯 | 編輯原始碼]

一個精確的 GC 應該只標記那些從指標引用的記憶體。它不應該標記從非指標型別引用的記憶體。移動 GC 還需要修改指標值,如果一個物件被重新定位。

哪個地址屬於哪個物件

[編輯 | 編輯原始碼]

如果掃描一個指標,應該知道物件的起始地址。因此,應該可以獲取 PLI(指標位置資訊),這是繼續進行的必要條件。

移動引用

[編輯 | 編輯原始碼]

在併發/增量 GC 實現中,移動引用是一個問題。

Object refa = new Object;
Object refb = null;
  1. 開始進行垃圾收集週期
  2. gc 掃描 refb,它為空
  3. gc 暫停或被另一個執行緒中斷
  4. 在其他地方:refb = refa; refa = null;
  5. gc 繼續(認為 refb 為空)
  6. gc 掃描 refa,它現在也為空
  7. GC:哦,沒有指向 obj 的 ref,我可以釋放它!
  8. 應用程式嘗試引用 refb,它不為空,並崩潰

這就是需要寫屏障的原因。在併發/增量 GC 中,每次寫入引用都需要使舊目標引用值的掃描失效。

保守堆疊掃描

[編輯 | 編輯原始碼]

由於精確掃描堆疊可能是一個難題(尤其是在考慮 C 介面時,例如從 C 程式碼到 D 程式碼的回撥),可以簡單地保守地掃描堆疊並“固定”所有可能從堆疊引用的物件。新的Mono GC就是這麼做的。

參見“保守掃描”部分。堆疊的處理方式類似於當前的標記和清除演算法。

物件雜湊碼

[編輯 | 編輯原始碼]

為了支援

Object.toHash()

在移動 GC 存在的情況下,需要將雜湊碼儲存在物件本身中。雜湊碼可以從物件的地址獲取,但由於它的地址可能發生變化,因此需要儲存它。

弱指標

[編輯 | 編輯原始碼]

應該考慮支援弱指標,它們是可能有用且經常被請求的功能。如果不再有指向物件的非弱指標,弱指標將被設定為 null。Java 和 C# 也支援弱指標。

指向原生型別和成員變數的指標

[編輯 | 編輯原始碼]

語言應該允許指向原生型別,但它們不應該與 GC 相關。這意味著,它們不應該阻止 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 就無需掃描暫存器,也不需要知道哪個暫存器包含指標值。

GC 介面

[編輯 | 編輯原始碼]

addRoot/removeRoot

[編輯 | 編輯原始碼]

enable/disable

[編輯 | 編輯原始碼]

針對併發實現。應該支援遞迴使用。這意味著它應該與計數器一起使用。計數器在每次呼叫 disable 時遞增,在每次呼叫 enable 時遞減。

pin/unpin

[編輯 | 編輯原始碼]

針對移動/壓縮型 GC 實現。

fullcollect

[編輯 | 編輯原始碼]

強制執行收集週期。呼叫將在收集週期結束後返回。在 Stop-the-world 實現中,這意味著所有執行緒將暫停。在併發實現中則不會。引用計數解決方案將立即返回。

華夏公益教科書