最終內容請以原文為準:wangwei.one/posts/396cf…
本文將為你介紹常見的GC演算法。
概述
常見的GC演算法主要有三種:標記-清除演算法
、標記-整理演算法
、複製演算法
。還有一種分代收集演算法
,這種演算法無非就是對記憶體的不同區域使用前面三種不同的演算法。
這三種GC演算法總體而言,都專注於幹兩件事情:
- 標記所有存活的物件。在垃圾收集中有一個叫做 標記(Marking) 的過程專門幹這件事。
- 清除所有死物件。這三種演算法的區別就在於清理死物件的實現方式上。
標記可達物件(Marking Reachable Objects)
現代JVM中所有的GC演算法,第一步都是找出所有存活的物件,如圖所示:
前面 講了 GC中 根(GC Roots) 的概念:
指的是指向物件指標的”起點”。
可作為GC Roots的節點主要在全域性性的引用(例如常量或類靜態屬性)與執行上下文(例如棧幀中的本地變數表)中,主要包括:
- 當前正在執行的方法裡的區域性變數和輸入引數
- 活動執行緒(Active threads)
- 記憶體中所有類的靜態欄位(static field)
- JNI引用
GC遍歷(traverses)記憶體中整體的物件關係圖(object graph),從GC根元素開始掃描,到直接引用以及間接引用(通過物件的 屬性域 )。所有GC訪問到的物件都被 (marked)為 存活物件 (上圖中的藍色標記)。而從GC根無法直接或間接訪問到的物件稱為 不可達的物件 (unreachable object) (上圖中的灰色標記),GC會在接下來的階段中清除掉這些不可達的物件。
在標記階段,需要注意幾點:
Stop The World(STW)
在標記階段,需要暫停所有應用執行緒,以遍歷所有物件的引用關係。因為這項分析工作必須在一個能確保一致性的快照中進行——這裡“一致性”的意思是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點上,不可以出現分析過程中物件引用關係還在不斷變化的情況,該點不滿足的話分析結果準確性就無法得到保證。這種情景叫做 Stop The World pause (全線停頓)。
暫停的時間,與堆記憶體的大小,物件的總數沒有直接關係,而是有存活物件(alive objects)的數量來決定。所以,增加堆記憶體的大小並不會直接影響標記階段佔用的時間。
OopMap
目前的主流Java虛擬機器使用的都是準確式GC(Exact VM),所以當執行系統停頓下來後,並不需要一個不漏地檢查完所有執行上下文和全域性的引用位置,虛擬機器應當是有辦法直接得知哪些地方存放著物件引用。在HotSpot的實現中,是使用一組稱為OopMap的資料結構來達到這個目的的,在類載入完成的時候,HotSpot就把物件內什麼偏移量上是什麼型別的資料計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和暫存器中哪些位置是引用。這樣,GC在掃描時就可以直接得知這些資訊了。
OopMap 是一種用於記錄位於Java棧上的物件引用的資料結構。 其主要目的是查詢位於Java棧上的 GC Roots,當Heap中物件移動時,會去更新對應的物件引用資訊。
Safe Point
程式並非在所有的地方都能停頓下來開始GC,只有達到安全點(Safe Point)才能暫定。如何在GC發生時,讓所有執行緒都"跑"到安全點上再停頓下來,這裡有兩種方案:
搶先式中斷(Preemptive Suspension)
不需要執行緒的執行程式碼主動配合,在GC發生時,首先把所有執行緒全部中斷,如果發現有執行緒中斷的地方不在安全點上,就恢復執行緒,讓它"跑"到安全點上。(這種方式幾乎沒有虛擬機器使用了)
主動式中斷(Voluntary Suspension)
當GC需要中斷執行緒的時候,不直接對執行緒操作,僅僅簡單地設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起。
Safe Region
當執行緒處理Sleep狀態或者Blocked狀態,這時執行緒無法響應JVM的中斷請求,也就無法"跑"到安全的地方去中斷掛起,JVM也不可能等到執行緒重新被分配CPU時間。這就需要安全區域來解決。
**安全區域(Safe Region)**是指在一段程式碼片段之中,引用關係不會發生變化。在這個區域中的任意地方開始GC都是安全的。
線上程執行到Safe Region中的程式碼時,首先標識自己已經進入了Safe Region,那樣,當在這段時間裡JVM要發起GC時,就不用管標識自己為Safe Region狀態的執行緒了。線上程要離開Safe Region時,它要檢查系統是否已經完成了根節點列舉(或者是整個GC過程),如果完成了,那執行緒就繼續執行,否則它就必須等待直到收到可以安全離開Safe Region的訊號為止。
刪除不可達物件(Removing Unused Objects)
各種GC演算法在刪除不可達物件時略有不同, 但總體可分為三類: 清除(sweeping)、整理(compacting)和複製 (copying)。
Sweep(清除)
Mark-Sweep(標記-清除)演算法是最基礎的收集演算法。在標記階段完成之後,所有不可達的物件佔用的空間都會被回收,用於下一次新物件的分配。
優點
- 演算法簡單,實現容易
缺點
- 碎片化(fragmentation)。在 GC 標記 - 清除演算法的使用過程中會逐漸產生被細化的分塊,不久後就會導致無數的 小分塊散佈在堆的各處。如果發生碎片化,那麼即使堆中分塊的總大小夠用,也會因為一個個的分塊都太小而不 能執行分配。
- 需要使用空閒連結串列 (freelist),來記錄所有的空閒區域,以及每個區域的大小。維護空閒表增加了物件分配時的開銷。
- 分配速度低下。GC 標記 - 清除演算法中分塊不是連續的,因此每次分配都必須遍歷空閒連結串列,找到足夠大的分塊。最糟的情況就是每次進行分配都得把空閒連結串列遍歷到最後。
Copy(複製)
為了解決效率問題,一種稱為“複製”(Copying)的收集演算法出現了,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。
優點
優秀的吞吐量
GC 標記 - 清除演算法消耗的吞吐量是搜尋活動物件(標記階段)所花費的時間和搜尋整體堆(清除階段)所花費的時間之和。
而 GC 複製演算法只搜尋並複製活動物件,所以跟一般的 GC 標記 - 清除算 法相比,它能在較短時間內完成 GC。也就是說,其吞吐量優秀。
尤其是堆越大,差距越明顯。GC 標記 - 清除演算法在清除階段所花費的時間會不斷增加, 但 GC 複製演算法就不會產生這種消耗。畢竟它消耗的時間是與活動物件的數量成比例的。
可實現高速分配
GC 複製演算法不使用空閒連結串列。這是因為分塊是一個連續的記憶體空間。因此,只要這個分塊大小不小於所申請的大小,那麼移動 $free 指標就可以進行分配了。不像 GC 標記 - 清除演算法那樣每次分配記憶體空間時,都要遍歷空閒連結串列,而且每次都要遍歷到最後一個分塊。
不會發生碎片化
GC 複製演算法每次回收垃圾物件是,都是對整個一半的空間進行回收,不像 GC 標記 - 清除演算法那樣,會留下碎片化的記憶體空間。
缺點
堆使用效率低下
GC 複製演算法把堆二等分,通常只能利用其中的一半來安排物件。也就是說,只有一半 堆能被使用。相比其他能使用整個堆的 GC 演算法而言,可以說這是 GC 複製演算法的一個重大 的缺陷。
Compact(整理)
標記-清除-整理演算法 (MarkSweepCompact)。標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。
優點
標記-清除-整理演算法 (MarkSweepCompact),將所有被標記的物件(存活物件),遷移到記憶體空間的起始處, 消除了標記清除演算法的缺點。
缺點
GC暫停時間會增加。因為需要將所有物件複製到另一個地方, 然後修改指向這些物件的引用。