1. 程式人生 > >GC 的三種基本實現方式

GC 的三種基本實現方式

參考資料《程式碼的未來》(作者: [日] 松本行弘)。

由於並非本人原著(我只是個“搬運工“),SO 未經本人允許請盡情轉載。

另外個人像說明一下這裡所說的GC指泛指垃圾回收機制,而單指Java或其他某種特定語言中的GC——可能具體語言中實現的垃圾回收實現機制會有所不同。下面是具體內容:

將記憶體管理,尤其是記憶體空間的釋放實現自動化,這就是GC(Garbage Collection)。  GC其實是個古老的技術,從20世紀60年代就開始研究,還發表了不少論文。這項技術在大學實驗室級別的地方已經應用了很長時間,但是可以說從20世紀90年代Java出現之後,一般程式設計師才有緣基礎到它,在此之前這項技術還只是少數人的專利。

術語定義

1,垃圾:  所謂垃圾(Garbage),就是需要回收的物件。作為編寫程式的人,是可以做出“這個物件已經不需要了“這樣的判斷,但是計算機是做不到的。因此如果程式(通過某個變數等等)可能會直接或間接的引用一個物件,那麼這個物件就被視為“存活“;與之相反,已經引用不到的則被視為“死亡“。將這些死亡物件找出來,然後作為垃圾進行回收,者就是GC的本質。 2,根  所謂的根(Root),就是判斷物件是否被引用的起始點。至於哪裡的才是根,不通的語言和編譯器都有不通的規定,但基本上是將變數和執行棧空間作為根。

主要GC實現方式:

標記清除方式

標記清除(Mark and Sweep)是最早開發出來的GC演算法(1960年)。它的原理非常簡單: 首先從根開始將可能被引用的物件用遞迴的方式進行標記,然後將沒有標記到的物件作為垃圾進行回收。

初始狀態: 初始狀態

標記階段:

標記階段1 標記階段2

清除階段:

這裡寫圖片描述

上述圖片顯示了標記清除演算法的大致原理。  “初始狀態“圖中顯示了隨著程式的執行而分配出一些物件的狀態,一個物件可以對其他的物件進行引用。

標記階段“圖中顯示了GC開始執行,從根開始可以被引用的物件上進行“標記“。大多數情況下,這種標記是通過物件內部的標誌(Flag)來實現的。於是,被標記的物件我們將它塗黑。

緊接著被標記的物件所能引用的物件也會被打上標記。重複這一步驟就可以從根開始可能被間接引用到的物件全部打上標記。到此為止的操作即被稱為——標記階段(Mark phase)。標記階段完成時,被標記的物件就是“存活“物件,反之為“死亡“物件

標記清除演算法的處理時間,是和存活物件數與物件總數的總和相關的。

作為標記清除的變形,還有一種叫做標記壓縮(Mark and Compat)的演算法,它不是將被標記的物件清除,而是將他們不斷壓縮。

複製收集方式

標記清除演算法有一個缺點,就是在分配了大量物件,並且其中只有一小部分存活的情況下,所消耗的時間會大大超過必要的值,這是應為在清除階段還需要對大量死亡物件進行掃描。

複製收集(Copy and Collection)則試圖克服這一缺點。在這種演算法中,會將從根開始被引用的物件複製到另外的空間中,然後,再將複製的物件所能夠引用的物件用遞迴的方式不斷複製下去

初始狀態(1)——舊空間:

初始狀態(1)

新空間的開闢(2)——新空間:

新空間的開闢(2)

複製物件(3)

複製物件(3)

如上圖:  (1)部分是GC開始前的記憶體狀態,者也同時代表著物件在記憶體中所佔用的“舊空間“。  圖(2)在舊空間以外開闢“新空間“並將可能從根被引用的物件複製到新空間中。  圖(3)從已經複製的物件開始再將可以被引用的物件逐個複製到新空間當中……隨著複製的進行,直到複製完成——最終“死亡“物件就留在了“舊空間“當中,接著將舊空間廢棄掉,這樣就可以將“死亡“物件所佔用的空間一口氣釋放出來,而沒有必要再次掃描“死亡“物件的必要。而等到下次GC操作是,這次所建立的“新空間“就成為了將來的“舊空間“了。

複製收集方式的過程相當於只存在於標記清除方式中的標記階段由於清除階段中需要對所有物件進行掃描,這樣如果在存在大量物件,且其中大量物件已經為“死亡“物件的情況下必然會造成不必要的資源和效能上的開銷。  而在複製收集方式中就不存在這樣的開銷。但是和標記相比,將物件複製一份的開銷相對要大,因此在“存活“物件相對比例較高的情況下,反而不利。

複製收集方式的另一個優點是:它具有區域性性(Locality)。在複製收集過程中,會按照物件被引用的順序將物件複製到新空間中。於是,關係較近的物件被放置在距離較近的記憶體空間中的可能性會提高,這樣被稱為區域性性。區域性性高的情況下,記憶體快取會更容易有效運作,程式的執行也能夠得到提高。

引用計數方式

引用計數方式是GC演算法中最簡單也最容易實現的一種,它和標記清除方式差不多是同一時間被髮明出來的。

它的原理是:在每個物件中儲存該物件的引用計數,當引用發生增減時對計數進行更新。  引用計數的增減,一般發生在變數複製,物件內容更新,函式結束(區域性變數不在被引用),等時間點。當一個物件的引用計數為0時,則說明它將來不會再被引用,因此可以釋放相應的記憶體空間。

(1) 引用計數(1)

(2) 引用計數(2)

(3) 引用計數(3)

如上圖:  (1)中所有物件都儲存著自己被多少個物件進行引用的數量(引用計數)——圖中右上角的的數字。  (2)當物件引用發生變化時,引用計數也會更者變化。在這裡圖中的物件B到D的引用實效後,物件D的引用計數變為0,由於物件D的引用計數變為0,因此D到E和C的引用計數也分=別減少。結果E的引用計數也變為0,於是想象E也會被釋放。  (3)引用計數為0的物件被釋放——“存活”物件被保留下來。而這個GC過程中不需要對所有物件進行掃描。

優點

  • 相比標記清除複製收集方式實現更容易。
  • 當物件不再被引用的瞬間就會被釋放。
  • 其他GC機制中,要預測一個物件何時會被釋放是很困難的,而在引用計數方式中則是立即被釋放。
  • 由於釋放操作是針對個別執行的,因此和其他演算法相比,由GC而產生的中斷時間就比較短。

缺點

這裡寫圖片描述

  • 無法釋放迴圈引用的物件。如上圖A,B,C三個物件沒有被其他物件引用,而是互相之間迴圈引用,因此他們的計數永遠不會為0,結果這些物件就永遠不會被釋放。
  • 必須在引用發生增減時對引用計數做出正確的增減,而如果漏掉或者更改了引用計數就會引發很難找到的記憶體錯誤。
  • 引用計數不適合並行處理。如果多個執行緒同時對引用計數進行增減的話,引用計數的值就可能會產生不一致的問題(結果就會導致記憶體錯誤),為了避免這樣的事情發生,對引用計數的操作必須採用獨佔的方式來進行。如果引用計數操作頻繁發生,每次使用都要使用加鎖等併發操作其開銷也不可小覷。