垃圾回收的區域
- 堆:Java 中絕大多數的物件都存放在堆中,是垃圾回收的重點
- 方法區:此中的 GC 效率較低,不是重點
由於虛擬機器棧的生命週期和執行緒一致,因此不需要 GC
物件判活
在垃圾收集器對堆進行回收之前,首先要做的就是判斷物件是否還存活,哪些已經成為垃圾。判活演算法主要有兩種:
- 引用計數法
- 可達性分析演算法
前者基本沒有什麼應用,不過 Python 還在使用。JVM 使用的都是可達性分析演算法
引用計數法
給物件新增一個引用計數器,當物件增加一個引用時計數器加 1,引用失效時計數器減 1。引用計數為 0 的物件可被回收
- 優點:原理簡單、效率高
- 缺點:物件之間迴圈引用(A.instance = B, B.instance = A),很難判斷
可達性分析演算法
通過一系列的稱為 GC Roots 的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到 GC Roots 沒有任何引用鏈相連時,則證明此物件是不可用的。從圖論的角度來說,就是 GC Roots 到這些物件是不可達的。所以此演算法叫做可達性分析演算法,如下圖所示:
在 Java 中,可固定作為 GC Roots 的物件主要包括:
- 當前虛擬機器棧中區域性變數表中的引用的物件
- 方法區中類靜態屬性引用的物件
- 方法區中的常量引用的物件
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任
finalize的自我救贖
Java提供 finalize() 方法,垃圾回收器準備釋放記憶體的時候,會將所有需要呼叫 finalize() 的物件放入一個 F-Queue,讓 Finalizer執行緒 來執行這些 finalize 方法,每個物件的 finalize 方法最多隻能被執行一次。在其中可以實現物件的拯救(讓它被別的物件引用即可)
不過目前 Java 官方已經不推薦使用 finalize 方法了
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任
各種引用
JDK 1.2 之前,引用中儲存的就是某個物件的起始記憶體地址,在這種定義之下,物件要麼被引用、要麼不被引用,對於一些“食之無味,棄之可惜”的物件就顯得無能為力。於是 JDK 1.2 之後對引用的概念進行了擴充,將引用分為了四種型別,強引用 > 軟引用 > 弱引用 > 虛引用
強引用(Strongly Reference)
就是最傳統的引用,比如 Object obj = new Object()
中,obj
就是一個強引用
如果物件被強引用標記,那麼垃圾回收器絕對不會回收它,遇到記憶體不足寧願丟擲 OOM
軟引用(Soft Reference)
對於軟引用標記的物件,垃圾回收器只有在記憶體不足的時候才會對其進行回收,在記憶體充足的情況下不會回收
示例程式碼如下:
public static void main(String[] args) {
String str = new String("Bird"); // 強引用
SoftReference<String> soft = new SoftReference<String>(str);
str = null; // 消滅強引用,讓其只有軟引用
System.out.println(soft.get()); // Bird
System.gc(); // 執行一次Full GC
System.out.println(soft.get()); // 還是Bird,此時記憶體是充足的,不會對其進行回收
}
軟引用可以用來標記一些記憶體敏感的物件,最典型的就是快取功能,只要記憶體充足就保留,記憶體不足就進行回收
弱引用(Weak Reference)
當垃圾回收器掃描到弱引用所標記的物件時,無論記憶體是否充足,都會將其回收
示例程式碼如下:
public static void main(String[] args) {
String str = new String("Bird"); // 強引用
WeakReference<String> soft = new WeakReference<String>(str);
str = null; // 消滅強引用,讓其只有軟引用
System.out.println(soft.get()); // Bird
System.gc(); // 執行一次Full GC
System.out.println(soft.get()); // null,雖然記憶體是充足的,也會對其進行回收
}
實際應用:WeakHashMap、ThreadLocal
虛引用(PhantomReference)
也稱幽靈引用,是最弱的一種引用,被虛引用標記的物件和沒有任何引用一樣,任何時候都可能被回收
虛引用主要是用來跟蹤物件被垃圾回收的活動
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任
垃圾回收理論前置知識
這些前置知識都是 HotSpot 中的實現細節,為後面介紹垃圾回收器做鋪墊。這裡我寫的簡單一點,自己看懂就行
根節點列舉
迄今為止,所有收集器在根節點列舉這一步驟時都是必須暫停使用者執行緒的,存在“Stop the World”(STW)。雖然後續耗時最長的可達性分析演算法耗可以與使用者執行緒一起併發,但根節點列舉不能併發,這是為了保證正確性
在 OopMap 的協助下,HotSpot 可以快速準確地完成 GC Roots 列舉,OopMap詳細知識可參見:JVM中的OopMap
安全點和安全區域
引用關係變化會導致 OopMap 內容變化,這樣的的指令非常多,如果為每一條這樣的指令都生成 OopMap,會消耗大量的記憶體。實際上 HotSpot 也的確沒有為每條指令都生成 OopMap,只是在“特定的位置”記錄了這些資訊,這些位置被稱為安全點(Safepoint)
有了安全點的設定,也就決定了使用者程式執行時,並非在程式碼指令流的任意位置都能夠停頓下來進行垃圾收集,而是強制要求必須執行到達安全點後才能夠停頓。安全點不能太少以至於讓收集器等待時間過長,也不能太多以至於增大執行時的記憶體負荷。安全點位置的選取以“是否具有讓程式長時間執行的特徵”為標準選定的,因為每條指令執行的時間都非常短暫,程式不太可能因為指令流長度太長這樣的原因而長時間執行,“長時間執行”的最明顯特徵就是指令序列的複用,例如方法呼叫、迴圈跳轉、異常跳轉等都屬於指令序列複用,所以只有具有這些功能的指令才會產生安全點
如何在垃圾收集發生時讓所有執行緒都跑到最近的安全點,然後停頓下來?HotSpot 的實現方式是:當垃圾收集需要中斷執行緒的時候,不直接對執行緒操作,僅僅簡單地設定一個標誌位,各個執行緒執行過程時會不停地主動去輪詢這個標誌,一旦發現標誌為真就自己在最近的安全點上主動中斷掛起。輪詢操作在程式碼中會頻繁出現,不過 HotSpot 中輪詢指令實現得十分精簡,不會影響效能
有些情況下,執行緒不在執行(即沒有分配處理器時間,最典型的就是處於Sleep狀態,或被阻塞),這時候執行緒無法響應虛擬機器的中斷請求,不能主動走到安全點去中斷掛起自己,這就必須引入安全區域來解決。安全區域是指能夠確保在某一段程式碼片段之中,引用關係不會發生變化,因此,在這個區域中任意地方開始垃圾收集都是安全的。可以把安全區域看作被擴充套件拉伸了的安全點。當使用者執行緒執行到安全區域裡面的程式碼時,首先會標識自己已經進入了安全區域,當這段時間虛擬機器發起垃圾收集,就不必在意這些安全區域內的執行緒是否到達安全點了。當執行緒要離開安全區域時,它要檢查虛擬機器是否已經完成了根節點列舉,如果完成了,那執行緒就當作沒事發生過,繼續執行;否則它就必須一直等待,直到收到可以離開安全區域的訊號為止
記憶集與卡表
為解決物件的跨代引用,引入了記憶集這一結構,用以避免把整個老年代加入 GC Roots 集合。記憶集是一種用於記錄從非收集區域指向收集區域的指標集合的抽象資料結構。具體實現有三種,根據記錄精度的不同,可以分為字長精度、物件精度、卡精度。比如卡精度就是記錄了一塊記憶體區域中的物件是否含有跨代引用,對應的具體實現就是卡表,這也是 HotSpot 的實現方式
卡表的每一個元素都對應著其標識的記憶體區域中一塊特定大小的記憶體塊,這個記憶體塊被稱作卡頁。在 HotSpot 中卡頁的大小為512位元組。一個卡頁的記憶體中通常包含不止一個物件,只要卡頁內有一個(或更多)物件的欄位存在著跨代引用,那就將對應卡表的陣列元素的值標識為1,稱為這個元素變髒,沒有則標識為0。在垃圾收集發生時,只要篩選出卡表中變髒的元素,就能輕易得出哪些卡頁記憶體塊中包含跨代指標,把它們加入 GC Roots 中
寫屏障
卡表如何維護,即卡表元素何時變髒、怎樣變髒?這個問題就是通過寫屏障來解決的
卡表元素何時變髒是很明確的——有其他分代區域中物件引用了本區域物件時,其對應的卡表元素就應該變髒,變髒時間點原則上應該發生在引用型別欄位賦值的那一刻
假如是解釋執行的位元組碼,那相對好處理,虛擬機器負責每條位元組碼指令的執行,有充分的介入空間;但在編譯執行的場景中呢?經過即時編譯後的程式碼已經是純粹的機器指令流了,這就必須找到一個在機器碼層面的手段,把維護卡表的動作放到每一個賦值操作之中
在 HotSpot 虛擬機器裡是通過寫屏障技術維護卡表狀態的。寫屏障可以看作在虛擬機器層面對“引用型別欄位賦值”這個動作的 AOP 切面,在引用物件賦值時會產生一個環形(Around)通知,供程式執行額外的動作,也就是說賦值的前後都在寫屏障的覆蓋範疇內。在賦值前的部分的寫屏障叫作寫前屏障,在賦值後的則叫作寫後屏障。寫後屏障更新卡表的操作如下:
void oop_field_store(oop* field, oop new_value) {
// 引用欄位賦值操作
*field = new_value;
// 寫後屏障,在這裡完成卡表狀態更新
post_write_barrier(field, new_value);
}
應用寫屏障後,虛擬機器就會為所有賦值操作生成相應的指令。一旦收集器在寫屏障中增加了更新卡表操作,無論更新的是不是老年代對新生代物件的引用,每次只要對引用進行更新,就會產生額外的開銷,不過這個開銷與 Minor GC 時掃描整個老年代的代價相比還是低得多
除了寫屏障的開銷外,卡表在高併發場景下還面臨著偽共享問題。JDK 7 之後,HotSpot 虛擬機器增加了一個新的引數 -XX:+UseCondCardMark
,決定是否開啟卡表更新的條件判斷。開啟會增加一次額外判斷的開銷,但能夠避免偽共享問題,兩者各有效能損耗,是否開啟要根據應用實際執行情況來進行測試權衡。先檢查再更新卡表的操作邏輯如下:
if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;
併發的可達性分析
GC Roots 列舉在 OopMap 的加持下,停頓時間已經是非常短暫了。但從 GC Roots 再繼續往下遍歷物件圖,這裡的停頓時間就與 Java 堆容量成正比例關係。堆越大,儲存的物件越多,物件圖結構越複雜,要標記更多物件而產生的停頓時間就越長。這個問題的解決方案就是——併發的可達性分析
在可達性分析和使用者執行緒併發執行的過程中,可能會出現兩種錯誤:
- 浮動垃圾:把原本消亡的物件錯誤標記為存活,這是可以容忍的錯誤,留到下一次回收即可
- 物件消失:把原本存活的物件錯誤標記為已消亡,這就是非常致命的後果,會導致程式錯誤!
經過證明,當且僅當以下兩個條件同時滿足時,會產生物件消失的問題:
- 賦值器插入了一條或多條從黑色物件(被垃圾回收器掃描過,認為是存活物件,且該物件所有的引用都被掃描過)到白色物件(尚未被掃描過)的新引用
- 賦值器刪除了全部從灰色物件(被掃描過,但該物件的引用還沒有全部被掃描過)到該白色物件的直接或間接引用
要解決併發的可達性分析時的物件消失問題,只需破壞這兩個條件的任意一個即可。由此分別產生了兩種解決方案:增量更新(Incremental Update)和原始快照(Snapshot At The Beginning,SATB)
- 增量更新要破壞的是第一個條件,當黑色物件插入新的指向白色物件的引用關係時,就將這個新插入的引用記錄下來,等併發掃描結束之後,再將這些記錄過的引用關係中的黑色物件為根,重新掃描一次。這可以簡化理解為,黑色物件一旦新插入了指向白色物件的引用之後,它就變回灰色物件了
- 原始快照要破壞的是第二個條件,當灰色物件要刪除指向白色物件的引用關係時,就將這個要刪除的引用記錄下來,在併發掃描結束之後,再將這些記錄過的引用關係中的灰色物件為根,重新掃描一次。這也可以簡化理解為,無論引用關係刪除與否,都會按照剛剛開始掃描那一刻的物件圖快照來進行搜尋
以上無論是對引用關係記錄的插入還是刪除,虛擬機器的記錄操作都是通過寫屏障實現的。在 HotSpot 虛擬機器中,增量更新和原始快照這兩種解決方案都有實際應用,譬如,CMS 是基於增量更新來做併發標記的,G1、Shenandoah 則是用原始快照來實現
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任
分代收集理論
理論假說
- 弱分代假說:絕大多數物件都是朝生暮死的
- 強分代假說:熬過越多次垃圾回收過程的物件越難以消亡
根據這兩個假說,就奠定了多款垃圾收集器的一致的設計原則:根據回收物件的年齡,將Java堆分出不同的區域。有了這些不同區域之後,就讓垃圾收集器針對特定區域進行收集,因而有了垃圾回收的分類和垃圾回收器的分類
不過這些記憶體區域在垃圾回收的邏輯上是隔離的,程式邏輯上卻並不隔離。如果要對單個區域進行收集,必須考慮跨代引用,如果是對新生代進行垃圾回收,最簡單的做法就是遍歷老年代,看看哪些老年代物件引用了新生代物件,將這些老年代物件也加入 GC Roots,不過這樣做的效率很低。隨後引出了第三條假說:
- 跨代引用假說:跨代引用相對於同代引用來說,僅佔極小部分
基於這條假說,就可以不同再為了少量的跨代引用去遍歷整個老年代的物件,只需要在新生代上建立一個全域性的資料結構——記憶集,標識出老年代的哪一塊記憶體存在跨代引用。之後,就可以根據記憶集直接查詢存在跨代引用的物件,而不用遍歷整個老年代。雖然在執行時需要在物件改變引用時維護這個記憶集,但相比於遍歷整個老年代,這部分多餘的開銷是划算的
垃圾回收分類
有了分代之後,相應的垃圾回收分類也被分為以下幾種:
Minor GC / Young GC
針對新生代的垃圾回收,發生頻率較高,執行速度也很快
觸發條件:Eden 空間不足,這時可能會發生空間分配擔保
Major GC / Old GC
針對老年代的垃圾回收,發生頻率很低。目前只有 CMS 收集器會有單獨收集老年代的行為(?)
Mixed GC
混合收集,針對整個新生代以及部分老年代的垃圾回收。目前只有 G1 收集器會有這種行為
Full GC
整堆收集,針對整個 Java 堆和方法區的垃圾回收
垃圾回收演算法
標記-清除演算法(Mark-Sweep)
首先標記所有需要回收的物件,然後統一清除這些物件所在的區域
優點:
- 空間利用率是 100%
缺點:
- 如果需要回收的物件有很多,需要大量的標記、清除動作,效率不高
- 會產生大量的記憶體碎片,導致以後可能因大物件分配而提前觸發垃圾收集
標記-複製演算法(Mark-Copy)
將記憶體劃分成兩等份,使用時只用一半。當這一半使用完,就將其中尚且存活的物件複製到另一個半區,並一次性清理使用的那半邊記憶體
優點:
- 不會出現記憶體碎片
缺點:
- 記憶體利用率低
- 會有大量的複製開銷
新生代物件 90% 都是朝生暮死,所以回收只需要使用 10% 的空間即可,不用按照 1:1 劃分空間,而是將記憶體分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活著的物件一次性地複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛才用過的 Survivor 空間。 HotSpot 虛擬機器預設 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的 90%(80%+10%),只有 10% 的記憶體會被“浪費”
標記-整理演算法(Mark Compact)
首先標記出所有需要回收的物件,等標記完成後,不是直接對垃圾物件進行清理,而是讓所有存活的物件都想記憶體空間的一端移動,最後直接清理掉邊界之外的記憶體
優點:
- 不用為了垃圾回收而預留額外的空間,空間利用率為 100%
- 不會留下記憶體碎片
缺點:
- 需要大量的標記、移動(複製)操作,效率不高
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任
垃圾收集器
垃圾收集演算法是記憶體回收的方法論,垃圾收集器是記憶體回收的實踐者,不同版本的垃圾收集器差別較大。接下來介紹的垃圾收集器,是 JDK 7 Update 4 之後,JDK 11 之前,OcracleJDK 中的 HotSpot 虛擬機器包含的全部可用的垃圾收集器(不是 OpenJDK)
總覽
收集器 | 收集區域 | 收集演算法 | 工作方式 | 特點 |
---|---|---|---|---|
Serial | 新生代 | 標記-複製 | 單執行緒 | 簡單高效、記憶體消耗低,適合單核CPU場景。但是有 STW 問題 |
ParNew | 新生代 | 標記-複製 | 並行 | Serial 的並行版本,適合多核 CPU,單核不如 Serial,因此適合服務端; 除了 Serial 之外只有 ParNew 可以和 CMS 配合工作,是啟用 CMS 後的預設新生代收集器 |
Parallel Scavenge | 新生代 | 標記-複製 | 並行 | 並行收集器,面向可控的吞吐量,適用於後臺計算任務; 可以控制吞吐量和最大停頓時間; 支援自適應的調節策略; 不能配合 CMS 工作 |
Serial Old | 老年代 | 標記-整理 | 單執行緒 | Serial 的老年代版本 |
Parallel Old | 老年代 | 標記-整理 | 並行 | Parallel Scavenge 的老年代版本,專門用於和其配合工作 |
CMS(Conc Mark Sweep) | 老年代 | 標記-清除 | 並行、併發 | 併發標記和清除階段都可以和使用者執行緒併發,停頓時間非常短; 對處理器資源非常敏感,併發時會佔用部分執行緒而降低總的吞吐量; 無法處理浮動垃圾,預留的記憶體如果不能滿足程式執行的需要,會產生併發失敗問題,這時會啟動後備預案 Serial Old 來執行一次 Major GC; 由於是基於標記-清除演算法,清除階段可以併發執行,但會產生記憶體碎片,可以開啟記憶體整理功能 |
G1(Garbage First) | 跨新生代、老年代 | 標記-複製(區域性上) + 標記-整理(整體上) | 並行、併發 | 基於 region 的堆記憶體劃分,執行回收價值優先的收集策略,實現了可預測的停頓時間模型; 新生代、老年代並不是固定的,而是多個 region 的動態組合; 和 CMS 追求儘可能低的停頓時間不同,G1 追求在可控的停頓時間下,獲得儘可能高的吞吐量,因此 G1 不會一次性回收所有垃圾 |
注:
- 並行:多個垃圾收集執行緒同時開展工作,此時使用者執行緒處於等待狀態
- 併發:垃圾收集的單/多執行緒與使用者的多執行緒同時執行
- 使用
jps -v
可以看到當前 JVM 程式使用的垃圾收集器,例如:-XX:+UseConcMarkSweepGC
表示使用了 CMS
垃圾收集器之間的組合使用關係:
注:連線代表可以組合的新生代、老年代垃圾回收器,紅叉表示 JDK 9 及之後取消了這種組合使用關係
Serial / Serial Old
Serial 是最古老的垃圾收集器,單執行緒,簡單高效,記憶體佔用(Memory Footprint)最少,適合單 CPU 伺服器。新生代、老年代的垃圾回收都會存在 STW
Serial Old 就是 Serial 的老年代版本,主要用途:
- 和 Serial 或者 Parallel Scavenge 搭配使用,不過 JDK 6 出現了 Parallel Old 之後,Parallel Scavenge 一般只與 Parallel Old 組合使用了
- 作為 CMS 收集器發生併發失敗(Concurrent Mode Failure)時,作為後備預案
Serial + Serial Old 組合:
使用方式:
-XX:+UseSerialGC
:使用 Serial + **Serial Old **的組合-XX:+UseParNewGC
:使用 ParNew + **Serial Old **的組合- 由於 JDK 6 出現了 Parallel Old,Parallel Scavenge 就不再和 Serial Old 組合使用了
- 作為 CMS 發生失敗的後備預案,這個是不用手動配置的
ParNew
ParNew 和 Serial 沒啥區別,唯一在於可以多執行緒並行垃圾回收,停頓時間比 Serial 更短。同樣會有存在 STW,但是更短暫。在單核 CPU 上不如 Serial,因為存在執行緒互動的開銷,但是多 CPU 下更加高效
ParNew + Serial Old 組合:
使用方式:
-XX:+UseParNewGC
:使用 ParNew + Serial Old 的組合-XX:+UseConcMarkSweepGC
:使用 ParNew + CMS 的組合
Parallel Scavenge / Parallel Old
可以利用多執行緒並行收集,但和其他並行收集器的最大區別在於,它的設計目標是吞吐量可控,而其他並行收集器(如 ParNew、CMS)的設計目標是縮短使用者執行緒的停頓時間
吞吐量(Throughout)計算公式:
追求低停頓時間適用於需要使用者互動的程式,提升使用者體驗;追求高吞吐量適合後臺計算任務,不需要互動,儘快完成運算
Parallel Scavenge 可以精準控制吞吐量:
-XX:MaxGCPauseMillis
:控制最大垃圾回收停頓時間。不是設定得越小越好,因為系統會將新生代調小,這樣停頓時間少了,但垃圾收集會更加頻繁,導致總的吞吐量下降-XX:GCTimeRatio
:直接設定吞吐量大小
Parallel Scavenge 還有一個最大的特點——自適應的調節策略,使用 -XX:+UseAdaptiveSizePolicy
開啟。如果開啟了該功能,就不需要指定新生代大小(-Xmn
)、Eden 和 Survivior 區的比例(-XX:SurvivorRatio
)、晉升老年代物件的大小(-XX:PretenureSizeThreshold
)等,虛擬機器會根據當前系統執行情況收集效能監控資訊,動態調整這些引數以達到使用者設定的優化目標,是更關注停頓時間 ( -XX:MaxGCPauseMillis
)還是更關注吞吐量(-XX:GCTimeRatio
),使用者另外只需設定基本的記憶體引數即可(如 -Xmn
設定堆最大容量),其他引數由虛擬機器自適應調節
Parallel Scavenge 不能與 CMS 配合工作,有兩點原因:
- 設計目標不同:CMS 是面向低停頓時間,而 Parallel Scavenge 是面向高吞吐量
- 技術上不同:Parallel Scavenge 收集器(也包括 G1)都沒有使用分代框架,而選擇另外獨立實現
JDK 5 之前能和 Parallel Scavenge 配合的只有 Serial Old,但 JDK 6 出現了 Parallel Old,它其實就是 Parallel Scavenge 的老年代版本,支援多執行緒併發收集,Parallel Scavenge 就和它組合使用,專門用於注重吞吐量的場合
Parallel Scavenge + Parallel Old 組合:
使用方式:
-XX:+UseParallelGC
:使用 Parallel Scavenge + **Parallel Old **的組合- 由於 JDK 6 之後出現了 Parallel Old,Parallel Scavenge 就不再和 Serial Old 組合使用了
CMS
CMS 和大多數並行收集器一樣,追求最短的停頓時間,比如網際網路站、B/S 系統上的應用。CMS 是基於標記-清除演算法,相比於基於標記-整理演算法的老年代收集器,過程更加複雜,但是最大的好處在於,垃圾回收過程不存在 STW,可以和使用者執行緒併發執行
ParNew + CMS 組合:
整個過程分為四步:
1、初始標記:標記 GC Roots 能夠直接關聯到的物件,會 STW,但由於速度很快(得益於OopMap),所以停頓時間很短
2、併發標記:從 GC Roots 直接關聯的物件開始進行可達性分析,過程較長,但可以和使用者執行緒併發執行,不存在STW
3、重新標記:修正併發標記期間,因使用者程式執行而導致標記產生變動的物件的標記,CMS 使用增量更新技術。會 STW,但可並行處理,所以停頓時間也很短
4、併發清除:刪除掉判斷為垃圾的物件,由於不需要像標記-整理演算法一樣移動存活物件,所以可以和使用者執行緒併發執行,不存在 STW 問題
CMS 的優點:
- 整個過程中耗時很長的併發標記、併發清除中,垃圾收集執行緒都可以和使用者執行緒併發執行,不存在 STW 問題。另外兩個階段不可以併發,但很短暫。因此總的來說,CMS 的記憶體回收可以和使用者執行緒併發執行,停頓時間很短
CMS 的缺點:
- 對處理器資源比較敏感,在併發階段會佔用一部分執行緒而導致程式變慢,降低總的吞吐量
- 無法處理浮動垃圾,可能出現併發失敗(Concurrent Mode Failure),而導致另一次完全 STW 的 GC:因為併發階段使用者執行緒還會產生新的垃圾,而它們可能會躲過標記過程而無法被回收,這部分垃圾就是浮動垃圾。由於浮動垃圾的存在,CMS 不能等老年代完全被填滿才開始 GC,必須預留足夠的空間給併發階段的使用者執行緒使用。可以通過
-XX:CMSInitiatingOccupancyFraction
調節 CMS 的觸發百分比,這個數值越大,回收頻率越低,效能越好。但如果太大,又會面臨另外一個風險:如果 CMS 預留的記憶體無法滿足程式執行的需要,就會出現併發失敗,此時JVM必須啟動後備預案:停頓使用者執行緒(STW),啟動 Serial Old 收集器對老年代進行垃圾收集,這樣會導致 STW。因此,CMS 觸發百分比不宜設定過高,會導致出現大量的併發失敗,降低效能 - 由於 CMS 基於標記-清除演算法,會產生記憶體碎片,給大物件分配帶來困難,因而提前觸發 Full GC。為解決該問題,CMS 提供
-XX:+UseCMSCompactAtFullCollection
開關引數,用於在 CMS 不得不進行 Full GC 時開啟記憶體碎片的整理過程,但這時需要移動存活物件,無法和使用者執行緒併發執行,所以存在 STW。為此又提供了一個引數-XX:CMSFullGCsBeforeCompaction
,要求 CMS 在執行一定次數不整理記憶體的 Full GC 之後,下一次進入 Full GC 之前先進行記憶體碎片的整理工作(預設為 0,表示每次進入 Full GC 都要進行碎片整理)
使用方式:
-XX:+UseConcMarkSweepGC
:使用 ParNew + **CMS **的組合- CMS 收集器發生併發失敗時,自動使用 Serial Old 作為後備預案
G1
可預測的停頓時間模型:在一個長度為 M 毫秒的時間內,消耗在 GC 上的時間大概率不超過 N 毫秒
G1 的分代模型——基於 region 的堆記憶體佈局
實現支援該模型的垃圾回收器,關鍵在於 G1 開創了基於 region 的堆記憶體佈局。之前的垃圾回收是要麼針對新生代,要麼針對老年代,要麼直接整堆收集。而 G1 可以針對堆中任何 region 進行回收,這就是 G1 的 Mixed GC 模式
基於 region 的堆記憶體佈局如下圖所示:
將連續的堆空間劃分為多個大小相等的 region,每個 region 都可以根據需要作為新生代的 Eden 空間、Survivor 空間,或老年代空間。收集器能夠對扮演不同角色的 region 採用不同的策略去處理,這樣無論是新生代物件,還是老年代物件,都能取得很好的收集效果
region 中還有一類特殊的 Humongous 區域,專門用來儲存大物件。G1 認為只要大小超過了一個 region 容量一半的物件,即可判定為大物件。每個 region 的大小可以通過引數 -XX:G1HeapRegionSize
來設定,取值範圍為 1MB~32MB,且應為 2 的 N 次冪。而對於那些超過了整個 region 容量的超級大物件,將會被存放在 N 個連續的 Humongous region 之中,G1 的大多數行為都把 Humongous region 作為老年代的一部分來看待
總之,雖然G1仍保留新生代和老年代的概念,但新生代和老年代不再是固定的,它們都是一系列 region(不需要連續)的動態集合
G1 的回收策略——回收價值優先
有了基於 region 的堆記憶體佈局後,G1 就可以支援可預測的停頓時間模型。G1 會跟蹤各個 region 裡面垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然後在後臺維護一個優先順序列表,每次根據使用者設定允許的 GC 停頓時間(-XX:MaxGCPauseMillis
來設定,預設 200 毫秒),優先處理回收價值大的 region,這也是“Garbage First”名字的由來
G1 要解決的問題
- 跨 region 引用問題:讓每個 region 維護各自的記憶集——雙向卡表結構。但是這種記憶集結構更加複雜,且由於 region 數量眾多,導致 G1 的記憶體佔用率更高,經驗上講 G1 的垃圾回收至少要消耗堆容量的 10%~20%。而 CMS 只要維護一份卡表,記錄老年代到新生代的引用即可,且卡表結構更加簡單,因此記憶體佔用率更低
- 併發標記階段,使用者執行緒會更新物件引用,打破原本的物件圖結構:CMS 使用增量更新演算法,而G1使用原始快照表(SATB)演算法
- 篩選回收階段,會有新的物件建立出來:G1 為每個 region 都設計了兩個 TAMS(Top At Mark Start)指標,將 region 中的一部分空間劃分出來用於併發回收過程中的新物件分配,新物件的地址必須再這兩個指標位置以上,G1會預設這個地址以內的物件是被隱式標記過的,即認為它們是存活的,不會納入回收範圍。不過如果記憶體回收的速度趕不上記憶體分配的速度,和 CMS 一樣,G1 也會出現併發失敗而導致 Full GC,進而產生長時間的 STW
G1 的回收過程
1、初始標記:標記 GC Roots 能夠直接關聯到的物件,並且修改 TAMS 指標的值,讓下一階段使用者執行緒併發執行時,能正確地在可用的 region 中分配新物件。會 STW,但由於速度很快,且這一步是借用 Minor GC 時同步完成的,實際上沒有額外的停頓
2、併發標記:從 GC Roots 直接關聯的物件開始進行可達性分析,過程較長,但可以和使用者執行緒併發執行,不存在 STW
3、最終標記:對使用者執行緒做另一個短暫的暫停,用於處理併發階段結束後仍遺留下來的最後少量的 SATB 記錄,即修正併發標記期間的標記變動情況,類似於 CMS 的重新標記階段。這一步會 STW,但可以並行執行,停頓時間很短暫
4、篩選回收:對各個 region 的回收價值進行排序,根據使用者期望的停頓時間來制定回收計劃,可自由選擇任意多個 region 構成回收集,然後把決定回收的 region 中的存活物件複製到空的 region 中,再清理掉整個舊 region 的全部空間。此操作涉及存活物件的移動,不能併發,必須將使用者執行緒停頓,再由多個 GC 執行緒並行回收
相對於 CMS ,G1 在回收階段並不是併發的,這是因為 G1 並不是純粹地追求低停頓時間,而是追求在可控的停頓時間下,獲得儘可能高的吞吐量。G1 在回收階段只會回收部分 region,因此可以做到停頓時間可控。並且為了最大化垃圾收集效率,保證吞吐量,所以選擇完全停頓使用者執行緒。這一點 G1 和 CMS 是有很大區別的
不過期望停頓時間不能設定得過小(-XX:MaxGCPauseMillis
,預設 200 毫秒),一般來說,回收階段佔到幾十到一百甚至接近 200 毫秒都很正常,但如果把停頓時間調得非常低,可能導致每次只能回收一小部分 region,垃圾回收速率跟不上記憶體分配速率,最終引發 Full GC,反而降低效能。從 G1 開始,垃圾回收器就開始追求回收速率迎合記憶體分配速率,而不追求一次性清空整個堆空間,所以說 G1 是 GC 發展的一個 milestone
G1 與 CMS 對比
- 相同點:它們都是設計目標都是:低停頓時間
- 不同點
- G1 的優勢:
- 可以指定期望最大停頓時間(
-XX:MaxGCPauseMillis
) - 整體上 G1 是基於標記-整理演算法,不會產生記憶體碎片
- 基於 region 的記憶體佈局 + 回收價值優先收集,垃圾回收策略更加合理
- 可以指定期望最大停頓時間(
- G1 的劣勢:
- 記憶體佔用率更高:G1 使用的卡表更加複雜,且每個 region 都必須維護一個卡表,導致 G1 的記憶集龐大,記憶體佔用率高;而CMS卡表簡單,且只用維護一份老年代到新生代的引用,記憶體佔用率低
- 執行負載更高:CMS 使用寫後屏障來更新卡表;G1 除了這之外,為了實現原始快照搜尋(SATB)演算法,還使用寫前屏障來跟蹤併發階段的指標變化情況
- G1 的優勢:
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任
記憶體分配與回收策略
物件優先在 Eden 中分配
大多數情況下,物件在新生代 Eden 區中分配。當 Eden 區沒有足夠空間進行分配時,虛擬機器將發起一次 Minor GC
大物件直接進入老年代
大物件就是指需要大量連續記憶體空間的 Java 物件。HotSpot 虛擬機器提供了 -XX:PretenureSizeThreshold
引數,指定大於該設定值的物件直接在老年代分配,這樣做的目的就是避免在 Eden 區及兩個 Survivor 區
之間來回複製,產生大量的記憶體複製操作
長期存活的物件將進入老年代
HotSpot 虛擬機器給每個物件定義了一個對
象年齡(Age)計數器,儲存在物件頭。物件通常在 Eden 區裡誕生,如果經過第一次
Minor GC 後仍然存活,並且能被 Survivor 容納的話,該物件會被移動到 Survivor 空間中,並且將其物件
年齡設為 1 歲。物件在 Survivor 區中每熬過一次 Minor GC,年齡就增加 1 歲,當它的年齡增加到一定程
度(預設為 15),就會被晉升到老年代中。物件晉升老年代的年齡閾值,可以通過引數 -XX:MaxTenuringThreshold
設定
動態物件年齡判定
為了能更好地適應不同程式的記憶體狀況,HotSpot 虛擬機器並不是永遠要求物件的年齡必須達到 - XX:MaxTenuringThreshold
才能晉升老年代,如果在 Survivor 空間中相同年齡所有物件大小的總和大於
Survivor 空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代,無須等到 -XX:MaxTenuringThreshold
中要求的年齡
空間分配擔保
發生 Minor GC 之前,虛擬機器必須先檢查老年代最大可用的連續空間是否大於新生代所有物件總
空間,如果這個條件成立,那這一次 Minor GC 可以確保是安全的。如果不成立,則虛擬機器會先檢視 - XX:HandlePromotionFailure
引數的設定值是否允許擔保失敗(Handle Promotion Failure);如果允
許,那會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大
於,將嘗試進行一次 Minor GC,儘管這次 Minor GC 是有風險的;如果小於,或者 -XX:HandlePromotionFailure
設定不允許冒險,那這時就要改為進行一次 Full GC
版權:本文版權歸作者和部落格園共有
轉載:歡迎轉載,但未經作者同意,必須保留此段宣告;必須在文章中給出原文連線;否則必究法律責任
重要的 GC 引數
引數 | 描述 |
---|---|
UseSerialGC | 虛擬機器執行在 Client 模式下的預設值,使用 Serial + Serial Old 的組合 |
UseParNewGC | 使用 ParNew + Serial Old 的收集器組合進行記憶體回收 |
UseConcMarkSweepGC | 使用 ParNew + CMS + Serial Old 的組合。Serial Old 收集器作為 CMS 收集器出現併發失敗後的後備收集器 |
UseParallelGC | 虛擬機器執行在 Server 模式下的預設值,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的組合 |
UseParallelOldGC | 使用 Parallel Scavenge + Parallel Old 的組合 |
UseG1GC | 使用 G1 收集器 |
SurvivorRatio | 新生代中 Eden 區域與 Survivor 區域的容量比值,預設為 8,表示 Eden : Survivor = 8 : 1 |
PretenureSizeThreshold | 直接晉升到老年代的物件大小,設定這個引數後,大於這個引數的物件將直接在老年代分配 |
MaxTenuringThreshold | 晉升到老年代的物件年齡,每個物件在堅持過一次 Minor GC 之後,年齡就增加 1,當超過這個引數值時就會晉升到老年代 |
UseAdaptiveSizePolicy | 動態調整 Java 堆中各個區域的大小以及進入老年代的年齡 |
HandlePromotionFailure | 是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個 Eden 和 Survivor 區的所有物件都存活的極端情況 |
ParallelGCThreads | 設定並行 GC 時進行記憶體回收的執行緒數 |
GCTimeRatioGC | 時間佔總時間的比率,預設值為 99,即允許 1% 的 GC 時間,僅在使用 Parallel Scavenge 收集器生效 |
MaxGCPauseMillis | 設定 GC 的最大停頓時間,僅在使用 Parallel Scavenge 收集器時生效 |
CMSInitiatingOccupancyFraction | 設定 CMS 收集器在老年代空間被使用多少後觸發垃圾收集,預設值為 68%,僅在使用 CMS 收集器時生效 |
UseCMSCompactAtFullCollection | 設定 CMS 收集器在完成垃圾收集後是否要進行一次記憶體碎片整理,僅在使用 CMS 收集器時生效 |
CMSFullGCsBeforeCompaction | 設定 CMS 收集器在進行若干次垃圾收集後再啟動一次記憶體碎片整理,僅在使用 CMS 收集器時生效 |
MaxGCPauseMillis | 設定單次垃圾回收預期的最大停頓時間,預設 200 毫秒,僅在使用 G1 收集器時生效 |
G1HeapRegionSize | 設定每個 region 的大小,取值範圍為 1MB~32MB,且應為 2 的 N 次冪,僅在使用 G1 收集器時生效 |
G1NewSizePercent | 設定新生代最小值,預設值 5%,僅在使用 G1 收集器時生效 |
G1MaxNewSizePercent | 設定新生代最大值,預設值 60%,僅在使用 G1 收集器時生效 |
ConcGCThreads | 設定併發標記階段,並行執行的執行緒數 |
其他引數:
引數 | 描述 |
---|---|
PrintGCDetails | 列印輸出詳細的 GC 收集日誌的資訊 |