1. 程式人生 > >Conservative GC (Part one)

Conservative GC (Part one)

目錄

保守式GC

保守式GC(Conservative GC)指“不能識別指標和非指標的GC”

不明確的根

不明確的根(ambiguous roots),下面三類都可以作為根。事實上是不明確的根

  • 暫存器
  • 呼叫棧
  • 全域性變數空間

以棧為例:在呼叫棧中有呼叫幀(call frame),呼叫幀裡面裝著函式內的區域性變數和引數值。不過局變數中如果有c語言裡面的int、double這樣的數值,也就會有void*這樣的指標。也就是說呼叫幀裡既有數值(非指標),也有指標

GC不能識別指標和非指標,這類叫做不明確根。這樣的GC演算法稱為保守式GC。

指標和非指標的區別

在不明確根這一條件下,GC不能準確識別指標和非指標。任意一個空間裡可能是根也可能不是根。這樣一來,GC在處理時就會出現大量指標識別錯誤因此保守式GC會檢查不明確的根,以某種程度的精度來識別指標。

  • 保守式GC使用下列幾種方式檢查根
    • 是不是被正確對齊的值:32的cpu下指標的值為4的倍數,64位為8的倍數。如果是其他情況,就會被識別為非指標。
    • 是不是指著堆內 :當分配了GC專用堆時,物件就會被分到堆裡。也就是說指向物件的指標一定指向這個堆。
    • 是不是指著物件的開頭:調查不明確的根內的值是不是指著物件的開頭。在標記清除那一節中,我們介紹了BiBOP.把物件按照固定大小對齊,核對物件的值是不是物件固定大小的倍數。

以上三種舉措,根據記憶體佈局和物件結構等檢查項也會有所變化。

貌似指標的非指標

遇到非指標和堆內的物件的地址一樣的情況。這個時候就無法識別這個值是非指標。這就是貌似指標的非指標(fasle pointer)。如圖示:

保守式GC將這種“貌似指標的非指標”看出指標物件。我們把這種情況叫做“指標的錯誤識別”。

打個比方,在採用 GC 標記 - 清除演算法的情況下,一找到貌似指標的非指標,程式就會將非指標指向的物件錯誤地識別為活動物件,對其進行標記。因為被錯誤識別的物件不會被廢棄而會被保留,所以遵守了GC的原則—“不廢棄活動物件”。像這樣,在執行GC時採取的是一種保守的態度,即“把可疑的東西看作指標,穩妥處理”,所以我們稱這種方法為“保 守式 GC”。

不明確資料結構

當基於不明確個根執行GC時,我們需要從物件的頭部獲取型別資訊。比如說C中的結構體設定flag,通過flag就可以識別。
如果能從頭中獲得結構體資訊,GC就能識別出物件域裡的值是指標還是非指標。以C為例,所有的域裡都包含型別資訊,只要沒有放入與型別不同含義的值,就有可能正確識別指標。

下列展示的結構體就會變成不明確的資料結構(ambiguous data structures)。

union{
    long n;
    void *ptr;
} ambiguous_data;

因為ambiguous_data是聯合體,所以它可能包括指標ptr,或者非指標n。那麼GC就無法準確識別出它是不是指標。當物件具有這樣的資料結構時,GC不僅會錯誤識別不明確根,也會錯誤識別域裡的值。

優點

  • 語言處理程式不依賴於GC

    易於編寫語言處理程式,處理程式基本不用在意GC就可以編寫程式碼。語言處理程式的實現者即使沒有意實到GC的存在,程式也會自己回收垃圾。因此語言處理程式的實現要比準確式GC簡單。

    缺點

  • 識別指標和非指標需要付出成本

    識別不明確的根和資料結構的值為“指標”或“非指標”時,我們需要付出一定的成本。

  • 錯誤識別指標會壓迫堆

    保守式 GC 會把被引用的物件錯誤識別為活動物件。如果這個物件存在大量的子物件,那麼它們一律都會被看成活動物件。因為程式把已經死了的非 活動物件看成了活動物件,所以垃圾物件會嚴重壓迫堆。

  • 能夠使用GC的演算法有限

    在無法正確識別指標的環境中,我們基本上不能使用GC複製演算法等移動物件的GC演算法。我們想用不明確的根這麼辦的話,就可能把非指標重寫了。此外,在物件內重寫指標時,也有可能因為不明確的資料結構而重寫了非指標。一旦重寫了非指標,就會產生 意想不到的 BUG。

準確式GC

準確式GC(Exact GC)和保守式GC正好相反,它是能正確識別指標和非指標的GC。

正確的根

準確式GC和保守式GC不同,它是基於能準確識別指標和非指標的“正確的根(exact roots)”來執行GC的。

建立根的方法有很多種,不過這些方法的共同點就是需要“語言處理程式的幫助”,所以正確的根的建立方法是依賴於語言處理程式實現的。(可能要等好久我才能記錄到那個地方。)

打標籤

第一個方法是打標籤(tag),目的是將不明確的根裡的所有非指標都與指標區別開來。打標籤的方法很多,最基本的低1位作為標籤的方法。

在32位cpu的情況下,指標的值是4的倍數,低2位一定是0,我們就利用這個特性。在前面提到引用1位計數法,這次我們使用它來識別指標和非指標。

  • 打標籤的具體方法:
    1. 將非指標int等,向左移1位(a << 1)
    2. 將低1位置位(a|1)

打標籤的時候我們需要注意,比如在對數值打標籤時,要注意不要讓資料溢位。如果資料溢位,我們就得再變換一個更大的資料型別。

如果用這種方式打標籤的話,所有數值都會是奇數。因此程式內進行計算時,必須取消標籤位在計算。

基本上打標籤和去標籤都是語言處理程式完成的,這就是之前說的需要語言處理程式的幫助。

為不明確的根裡的所有非指標打標籤後,GC就能正確的識別指標和非指標了即正確識別指標。

不把暫存器和棧等當做根

不把暫存器和棧等不明確因素當做根,而是在處理程式裡建立根。

具體來說就是,建立一個正確的根來管理,這個正確的根在處理程式裡只集合了mutator可能到達的指標,然後以他為基礎進行GC。

優點

首先,準確式GC完全沒有保守式GC固有的問題--錯誤識別指標。此外它還可以實現GC複製演算法等移動物件的演算法。準確式GC可以正確的識別指標,所以即使移動物件,重寫根這個物件也是正確的。

缺點

在建立語言處理程式時,必須照顧到GC。這會給實現演算法帶來一定的負擔。在處理上也比較麻煩。

此外,建立正確的根就必須付出一定代價,比如說打標籤。這實際關係到語言的處理速度。

間接引用

保守式 GC 有個缺點,就是“不能使用 GC 複製演算法等移動對 象的演算法”。解決這個問題的方法之一就是“間接引用”。

經由控制代碼引用物件

在保守式GC中有一個問題,無法使用垃圾回收演算法。原因就是因為就不能明確的根,重寫的物件有可能是非指標。

這個問題可以通過控制代碼(handle)來間接的處理物件。

從下圖可以看出,根和物件之間有控制代碼。每個物件都有一個控制代碼,他們分別持有指向這些物件的指標。並且區域性變數和全域性變數這些不明確的物件的根裡沒有隻想物件的指標,只裝著指向控制代碼的指標。也就是說,有mutator操作物件時,要通過經由控制代碼的間接引用來執行處理。

採用控制代碼之後,就算是移動物件,也只需要修改控制代碼裡的指標就行了。也就是說通過控制代碼間接訪問物件。

在物件內沒有經由控制代碼指向別的物件。只有在從根引用物件時,才會經由控制代碼。

使用間接引用的GC演算法不是準確式GC。因為間接引用是以不明確的根為基礎執行的GC,所以還不能正確識別指標和非指標。也就是說,還是會發生錯誤識別的情況。所以他也是保守式GC。

優缺點

優點:使用間接引用有可能實現GC複製演算法,所以GC複製演算法的優點就是它的優點。例如消除碎片化。
缺點:物件都經由控制代碼簡介引用,所以會拉低訪問記憶體物件的速度,這關係到整個語言處理程式的速度。