1. 程式人生 > >垃圾收集演算法實現與垃圾收集器(筆記)

垃圾收集演算法實現與垃圾收集器(筆記)

一、HotSpot中垃圾收集的演算法實現

1、列舉根節點

1.1、從可達性分析中從GC Roots節點找引用鏈這個操作為例,可作為GC Roots的節點主要在全域性性的引用(例如常量和類靜態屬性)與執行上下文(例如棧幀中的本地變量表)中,現在很多應用僅僅方法區就有數百兆,逐個檢查所有引用的話必然會消耗很多時間。

1.2、可達性分析對執行時間的敏感還體現在停頓上,因為這項工作分析工作必須在一個能確保一致性的快照中進行——這裡一致性的意思是指在整個分析期間物件引用關係不再發生變化。這點是導致GC 進行時必須停頓所有Java執行執行緒(“Stop The World”)的重要原因。

1.3、準確式GC:Java虛擬機器不需要檢查所有執行上下文和全域性的引用變數,它是有辦法知道哪些地方存著物件的引用(在HotSpot中,使用一組稱為OopMap資料結構來達到這個目的,在類載入完成的時候HotSpot就把物件內什麼偏移量上存的是什麼型別的資料計算出來,在JIT編譯的過程中,也會在特定的位置記錄下棧和暫存器中哪些位置是引用)。

2、安全點

2.1、定義:HotSpot沒有為每條指令都生成OopMap(需要大量的額外空間),它只是在“特定的位置”記錄了這些資訊,這些位置就稱為“安全點”。程式只有執行到安全點才能停下來進行GC工作。(SafePoint既不能太少,以致於讓GC等待的時間太長,也不能太多以致於過分增大執行時的負荷。)

2.2、選定標準——“是否具有讓程式長時間執行的特徵”

每條指令的執行時間都非常短暫,程式不太可能因為指令流長度太長這個原因而長時間執行。“長時間執行”的最明顯特徵就是指令序列複用,例如方法呼叫、迴圈跳轉、異常跳轉等,所以具有這些功能的指令才會產生SafePoint。(?)

這些特定的位置主要在:


A、迴圈的末尾
B、方法臨返回前 / 呼叫方法的call指令後
C、可能拋異常的位置

2.3、執行緒如何跑到安全點停頓

A、搶先式中斷:在GC發生時強行中斷所有執行緒,如果發現執行緒中斷的地方不在安全點上,再恢復執行緒,讓它跑到安全點。

B、主動式中斷(主流):GC不需要對執行緒進行操作,僅僅簡單地設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起。(具體的方法就是,虛擬機器把某個記憶體頁設定為不可讀,執行緒執行到相應的test指令時就會產生一個自陷異常訊號,在預先註冊的異常處理器中暫停執行緒實現等待)

3、安全區域

安全點的擴充套件。解決程式“不執行”的問題(即執行緒處於sleep或者Blocked等狀態,無法響應JVM的中斷請求,“走”到安全的地方去掛起,JVM也不太可能等待執行緒重新被分配CPU空間)

定義:安全區域是指在一段程式碼片段之中,引用關係不會發生改變。在這個區域中的任何地方開始GC都是安全的。

過程:當執行緒執行到SafeRegion中的程式碼時,首先標誌自己已經進入Safe Region了,那樣,當在這段時間裡JVM要發起GC時就不用管標識自己為Safe Region狀態的執行緒了。當執行緒要離開安全區域時,它要檢查系統是否已經完成了跟節點列舉(或者是整個GC過程),如果完成了,那執行緒就繼續執行,否則它就必須等待直到收到可以安全離開Safe Region的訊號為止。

二、垃圾收集器

不同廠商、不同版本使用的垃圾收集器都不太一樣。HotSpot的垃圾收集器如下圖(圖中相互連線表示可以搭配使用,虛擬機器所處的區域則表示它是老年代還是新生代的收集器)——目前還有一種適用於任何場景的垃圾收集器,因此我們要根據不同的場景來選擇合適的垃圾收集器。

1、Serial收集器

1.1、特性:單執行緒收集器。A、只會使用一個CPU或一條收集執行緒去完成垃圾收集工作;B、在它進行垃圾收集期間必須暫停其他所有的工作執行緒,直到它收集結束。

1.2、不足:“Stop The World”會帶來很差的使用者體驗。

1.3、優點:簡單高效。(因此它目前還是虛擬機器執行在Client模式下的預設新生代收集器。)

對於限定單個CPU的環境來說,Serial收集器由於沒有執行緒互動的開銷,專心做垃圾收集m,自然可以獲得最高的效率。在使用者的桌面應用場景中,分配給虛擬機器的記憶體一般不會很大,收集幾十兆甚至幾百兆的新生代,停頓時間完全可以控制在幾十毫秒最多一百毫秒之內。

1.4、使用場景:Client模式+新生代

2、ParNew收集器

2.1、特性:多執行緒收集器(Serial收集器的多執行緒版本)。使用多條執行緒進行垃圾收集。

2.2、不足:ParNew在單CPU的環境中絕對不會有比Serial收集器更好的效果。

2.3、優點:A、目前只有它和Serial能與CMS收集器配合工作(CMS第一次實現了讓垃圾收集執行緒與使用者執行緒(基本上)同時工作);B、隨著可以使用的CPU的數量的增加,它可以更有效地利用系統資源。——它預設開啟的收集執行緒數與CPU的數量相同,也可以使用-XX:ParallelGCThreads引數來限制垃圾收集的執行緒數。

2.4、使用場景:Server模式+新生代

3、Parallel Scavenge收集器

3.1、特性:關注點不在於儘可能縮短垃圾收集時使用者執行緒的停頓時間,而是達到一個可控的吞吐量。

吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間)。

停頓時間越短越適合需要與使用者互動的程式,良好的響應速度可以提升使用者體驗,而高吞吐量則可以高效地利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。

3.2、引數設定

A、-XX:MaxGCPauseMillis(如果更關注最大停頓時間,就設定這個引數)

大於0的毫秒數,收集器儘可能地保證記憶體回收時間不超過設定值。(但是並不是把這個引數設定得越小越好,因為GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的。比如,系統把新生代調小一些,收集300M的空間肯定要比收集500M的空間來得快。但是這也直接導致垃圾收集次數變得更加頻繁。)

B、-XX:GCTimeRatio(如果更關注最大吞吐量,就設定這個引數)

大於0小於100的整數,也就是垃圾收集時間佔總時間的比率,相當於是吞吐量的倒數。如果把此引數設定為19,那允許的最大GC時間就佔總時間的5%(即1/(1+19))。預設值為99,即允許最大1%的垃圾收集時間。

C、-XX:SurvivorRatio

開啟這個引數,就不需要手動去設定新生代的大小、Eden與Survivor的比例、晉升老年代物件的大小等細節引數了。虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量。——GC自適應的調節策略

3.3、優點:“吞吐量優先”收集器

3.4、適用場景:Server模式+新生代

4、Serial Old 收集器

4.1、特性:Serial收集器的老年代版本

4.2、適用場景:

A、Client模式+老年代(主要是給Client模式下的虛擬機器用)

B、Server模式+老年代:(Server模式下的兩大用途:一是在JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用;二是作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。)

5、Parallel Old 收集器

5.1、特性:Parallel Scavenge收集器的老年代版本。使用多執行緒以及“標記-整理演算法”。

5.2、優點:與Parallel Scavenge配合使用。之前沒有Parallel Old的時候,如果新生代選擇了Parallel Scavenge,那麼老年代就只能使用Serial Old,但是由於Serial Old在伺服器應用效能上的“拖累”,使用Parallel Scavenge收集器也未必能在整體應用上獲得吞吐量最大化的效果。

5.3、優點:“吞吐量優先”收集器

5.4、使用場景:Server模式+老年代

6、CMS收集器——Concurrent Mark Sweep(併發低停頓收集器)

6.1、開發目標:獲得最短回收停頓時間。

6.2、實際應用場景:網際網路站或者B/Z系統的服務端等尤其重視服務響應速度的應用上。

6.3、特性:使用“標記-清除”演算法

6.4、具體的收集過程

初始標記(CMS initial mark):

需要“Stop The World”。僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快。

併發標記(CMS concurrent mark):

不需要“Stop The World”。進行GC Roots tracing,這個需要的時間比較長,但是由於這個階段允許併發執行使用者程序,因此問題不大。

重新標記(CMS remark):

需要“Stop The World”。修正併發標記期間因使用者程式繼續運作而導致產生變動的那一部分物件的標記記錄。這個停頓時間會比初始標記的時間要長一些,但是遠比並發標記的時間短。

併發清除(CMS concurrent sweep):

不需要“Stop The World”。回收標記的物件。

6.5、優點:併發收集、低停頓。

6.6、缺點:

A、CMS收集器對CPU資源非常敏感。面向併發設計的程式都對CPU資源比較敏感。在併發階段,它雖然不會導致使用者執行緒停頓,但是會因為佔用了一部分CPU資源而導致應用程式變慢,總吞吐量會降低。CMS預設啟動的回收執行緒數是(CPU數量+3)/4。(“增量式併發收集器”可以在併發標記和清除的時候讓GC執行緒和使用者執行緒交替進行,減少被GC執行緒佔用的CPU資源,但是回收效果很一般,已被棄用)

B、CMS無法處理浮動垃圾。在CMS併發清理階段使用者現成產生的新垃圾就被稱為“浮動垃圾”,CMS無法在當次收集處理它們,只好等待下一次GC時在清理掉。CMS不能等到老年代幾乎填滿了在進行收集(必須留一部分給併發過程中產生的新物件用)。相應的引數為-XX:CMSInitiatingOccupancyFraction。JDK1.5預設設定為68%,JDK1.6預設設定為92%。要是CMS執行期間預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機器將啟動後備預案:臨時啟用Serial Old來重新進行老年代的垃圾收集。(引數不能設定太高,否則容易導致大量的“Concurrent Mode Failure”,效能反而降低)

C、CMS採用“標記-清除”演算法,在垃圾收集結束後會有大量的空間碎片產生。解決方案:開啟-XX:UseCMSCompactAtFullCollection引數。當在CMS快要頂不住進行Full GC時開啟記憶體碎片的合併整理過程。——這個過程是無法併發的,空間碎片的問題解決了,但是停頓時間不得不變長。另外一個引數-XX:CMSFullGCsBeforeCompaction。這個引數用於設定執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的(預設值為0,表示每次進入Full GC都要進行碎片壓縮)。

7、G1收集器

7.1、設計目標:面向服務端應用的垃圾收集器,未來可以替換掉CMS。

7.2、特點:

A、併發與並行:充分利用多CPU的硬體優勢,使用多個CPU來縮短Stop-The-World的時間。部分其他收集器原本需要停頓執行緒執行的GC動作,G1收集器仍然可以通過併發讓它繼續執行。

B、分代收集:它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間的物件以獲取更好的收集效果。

C、空間整合:從整體上看,採用了“標記-整理”演算法;從區域性上看,採用了“複製”演算法。不會產生空間碎片

D、可預測的停頓:G1可以建立可預測的停頓時間模型,能讓使用者指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。——因為它可以有計劃地避免在整個堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的回收時間,優先回收價值大的Region。

7.3、記憶體佈局:G1收集器將整個Java堆分為多個大小相等的獨立區域,雖然還保留有新生代和老年代的概念,但它們已經不再是物理隔離的了,都是一部分Region的集合。

7.4、掃描方式:使用Remembered Set來避免進行全堆掃描。每個Region都有一個Remembered Set。一旦有對Reference型別的資料進行寫操作,就會檢查Reference引用的物件是否位於不同的Region中。如果是,就把引用資訊記錄到被引用物件所屬的
Region的Remembered Set中。在進行記憶體回收時,在GC根節點的列舉範圍中加入Remembered Set即可。

7.5、運作步驟

A、初始標記(Initial Marking)

需要Stop-The-World。僅僅只是標記一下GC Roots能直接關聯到的物件,並修改TAMS(Next Top at Mark Start)的值,讓下一個階段使用者程式併發執行時能在正確的Region中建立新物件。

B、併發標記(Concurrent Marking)

可與使用者程式併發執行。從GC Roots中開始對堆進行可達性分析,找出存活的物件。

C、最終標記(Final Marking)

修正併發標記期間由於使用者程式繼續執行而導致標記產生變動的那一部分標記記錄。,虛擬機器將這段時間物件變化的資訊記錄線上程Remembered Set中,這階段需要停頓執行緒,但是可並行執行(應該只是垃圾收集執行緒並行執行)。

D、篩選回收(Live Data Counting and Evacuation)

首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來制定回收計劃,這個階段其實也可以做到和使用者執行緒一起併發執行,但是因為只回收一部分Region,時間是使用者可控制的,而且停頓使用者執行緒將大幅提高收集效率。

部落格內容來自《深入理解Java虛擬機器》