JVM筆記-垃圾收集演算法與垃圾收集器
1。 一些概念
1。1 垃圾&垃圾收集
垃圾:在 JVM 語境下,“垃圾”指的是死亡的物件所佔據的堆空間。
垃圾收集:所謂“垃圾收集”,就是將已分配出去、但不再使用的記憶體回收回來,以便能再次分配。
1。2 物件是否死亡
如何判斷一個物件是否死亡(即不可能再被任何途徑使用)?通常有以下兩種方法:
1。2。1 引用計數法
引用計數法(Reference Counting):為每個物件新增一個引用計數器,用來統計指向該物件的引用個數。當有地方引用它時,計數器加一;引用失效時減一。當某個物件的引用計數為零時,說明該物件已死亡,便可以被回收了。
主要優點:原理簡單,判定效率高。
主要缺點:無法解決迴圈依賴的問題(物件之間相互迴圈引用)。
1。2。2 可達性分析演算法
可達性分析(Reachability Analysis)的基本思路如下:
透過一系列稱為 GC Roots 的根物件作為起始節點集,從這些節點開始,根據引用關係向下搜尋,搜尋過程所走過的路徑稱為“引用鏈”(Reference Chain),若某個物件到 GC Roots 間沒有任何引用鏈相連(或者用圖論的話來說就是從 GC Roots 到這個物件不可達時)則證明此物件是不可能再被使用的。
示意圖如下:
GC Roots 可理解為「堆外指向堆內的引用」。在 Java 技術體系中,固定可作為 GC Roots 的物件包括以下幾種:
虛擬機器棧(棧幀中的本地變量表)中引用的物件
方法區中類靜態屬性引用的物件(比如引用型別的靜態變數)
方法區中常量引用的物件(比如字串常量池的引用)
本地方法棧中 Native 方法引用的物件
JVM 內部的引用(例如:基本資料型別的 Class 物件、常駐異常、系統類載入器)
同步鎖(synchronized 關鍵字)持有的物件
反應 JVM 內部情況的 JMXBean、JVMTI 中註冊的回撥、原生代碼快取等
1。3 引用的分類
無論是引用計數法還是可達性分析演算法,二者都離不開「引用」。JDK 1。2 之後,Java 中「引用」的概念得以擴充,主要分為以下四種:
強引用(Strongly Reference)
例如:
Object obj = new Object()
特點:無論任何情況,只要強引用存在,垃圾收集器永不回收被引用的物件
軟引用(Soft Reference)
場景:用於一些還有用、但非必須的物件
時機:被軟引用關聯的物件,在系統將發生 OOM 前,回收這些記憶體
實現:java。lang。ref。SoftReference
弱引用(Weak Reference)
場景:非必須物件,比軟引用更弱
時機:被弱引用關聯的物件只能生存到下一次垃圾收集發生(無論記憶體是否充足都會回收)
實現:java。lang。ref。WeakReference
虛引用(Phantom Reference)
又稱“幽靈引用”或“幻影引用”
特點:最弱的引用,是否存在完全不會影響其生存時間,無法透過它獲取物件例項
唯一目的:該物件被回收時收到一個系統通知
實現:java。lang。ref。PhantomReference
1。4 回收方法區
《Java 虛擬機器規範》並未要求虛擬機器在方法區實現垃圾收集。方法區垃圾收集“價效比”較低。
但在大量使用反射、動態代理、CGLib 等位元組碼框架,動態生成 JSP 及 OSGi 這類頻繁自定義類載入器的場景中,通常都需要 JVM 具備型別解除安裝的能力,以避免方法區記憶體壓力過大。
方法區垃圾回收的主要內容包括:廢棄的常量和不再使用的型別。它們的判定條件如下:
廢棄的常量
無任何物件引用常量池中的常量
虛擬機器中無任何其他地方引用該字面量
不再使用的型別
該類所有的例項(包括子類)都已被回收
該類的類載入器已被回收
該類的 Class 物件任何地方未被引用,任何地方無法透過反射訪問該類的方法
虛擬機器引數:
# 是否對型別回收
-Xnoclassgc
# 檢視類載入和解除安裝
-verbose:class -XX:+TraceClassLoading -XX:+TraceClassUnLoading
2。 垃圾收集演算法
從如何判定物件消亡的角度出發,垃圾收集器可分為“引用計數式垃圾收集”(Reference Counting GC)和“追蹤式垃圾收集”(Tracing GC)兩大類,也稱“直接垃圾收集”和“間接垃圾收集”。
這裡的垃圾回收演算法都屬於“追蹤式垃圾收集”的範疇。
2。0 分代收集理論
當代商業虛擬機器的垃圾收集器,多數都遵循了“分代收集”(Generational Collection)的理論。
分代收集理論,實質是一套符合大多數程式執行實際情況的經驗法則,有如下三條:
弱分代假說(Weak Generational Hypothesis):絕大多數物件都是朝生夕滅的。
強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的物件就越難以消亡。
跨代引用假說(Intergenerational Reference Hypothesis):跨代引用相對於同代引用來說僅佔極少數。
根據以上幾條規則,可以推測:
若一個區域中大多數物件都是朝生夕滅,把它們集中在一起,每次回收只需關注如何保留少量存活的物件;
若一個區域剩下的都是難以消亡的物件,把它們集中在一起,便可以較低的頻率回收該區域。
如何解決跨代引用問題?
依據跨代引用假說,為了解決極少數跨代引用,只需在新生代建立一個“記憶集(Remembered Set)”,把老年代劃分為若干小塊,標識出哪一塊記憶體會存在跨代引用,此後發生 Minor GC 時,只有包含跨代引用的小塊記憶體中的物件才會被加入到 GC Roots 進行掃描(避免掃描整個老年代)。
一些 GC 概念:
部分收集(Partial GC):目標不是完整收集整個 Java 堆的垃圾收集
新生代收集(Minor GC/Young GC):只是新生代的垃圾收集。
老年代收集(Major GC/Old GC):只是老年代的垃圾收集。目前只有 CMS 收集器會有單獨收集老年代的行為。
混合收集(Mixed GC):是收集整個新生代以及部分老年代的垃圾收集。目前只有 G1 收集器會有這種行為。
整堆收集(Full GC):收集整個 Java 堆和方法區的垃圾收集。
下面介紹常見的垃圾收集演算法。
2。1 標記-清除演算法
標記-清除(Mark-Sweep)演算法:最早、最基礎的演算法,分為“標記”和“清除”兩個階段:
標記出所有需要回收的物件(或反之);
在標記完成後統一回收所有被標記的物件(或反之)。
後續的收集演算法大多都是以“標記-清除”演算法為基礎,對其缺點進行改進而得。
該演算法的示意圖如下:
優缺點分析:
主要優點:實現簡單;
主要缺點:
執行效率不穩定,標記和清除過程效率都不高(標記物件較多時);
記憶體空間碎片化問題(碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作)。
圖片來自:
http://www。
cnblogs。com/dolphin0520
/p/3783345。html
2。2 標記-複製演算法
簡稱複製(Copying)演算法。現在的商用 Java 虛擬機器大都優先採用了這種收集演算法回收新生代。
“半區複製”(Semispace Copying)演算法將可用記憶體按容量分為大小相等的兩塊,每次只使用其中的一塊。當這一塊用完時,就將還存活的物件複製到另外一塊上,然後再把已使用的記憶體空間一次清理掉。
該演算法主要為了解決“標記-清除”演算法面對大量可回收物件時執行效率低的問題。
示意圖如下:
“半區複製”演算法的優缺點:
優點:實現簡單,執行高效且不易產生記憶體碎片;
缺點:可用記憶體縮小為原來的一半,空間浪費太多。
PS: 一般不需要按照 1:1 的比例劃分記憶體空間,而是將記憶體分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor。當回收時,將 Eden 和 Survivor 中還存活的物件一次性地複製到另外一塊 Survivor 空間上,最後清理掉 Eden 和剛才用過的 Survivor 空間。
HotSpot 虛擬機器預設 Eden 和 Survivor 的大小比例是 8:1 (即“浪費”了 10% 的新生代空間)。
由於無法保證每次每次回收都只有不多於 10% 的物件存活,當 Survivor 空間不夠用時,需要依賴其他記憶體(大多指老年代)進行分配擔保(Handle Promotion),也就是直接進入老年代(相當於“兜底方案”)。
2。3 標記-整理演算法
複製演算法在物件存活率較高時就要進行較多的複製操作,效率將會降低。
標記-整理(Mark-Compact)演算法:標記過程與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。
標記-清除演算法與標記-整理演算法區別:前者是一種非移動式的回收演算法,後者是移動式的。
示意圖:
主要問題:
移動存活物件:回收記憶體時會更復雜(Stop The World);
不移動存活物件:分配記憶體時會更復雜(空間碎片問題)。
2。4 分代收集演算法
當前商業虛擬機器的垃圾收集都採用“分代收集”(Generational Collection)演算法(不是一種具體的演算法實現,可以理解為「組合模式」):根據物件存活週期的不同,將記憶體劃分為幾塊。
一般把 Java 堆分為新生代和老年代,根據各個年代的特點採用最適當的收集演算法。
新生代
每次垃圾收集時,都有大批物件死去,只有少量存活,採用複製演算法(只需複製少量存活物件)。
老年代
物件存活率較高,沒有額外空間對它進行分配擔保,必須使用“標記-清除”或“標記-整理”演算法進行回收。
3。 垃圾收集器
前面的收集演算法只是記憶體回收的方法論,而垃圾收集器才是記憶體回收的具體實現(可理解為“介面”與“實現類”的關係)。
3。1 Serial 收集器
Serial 收集器是最基礎、歷史最悠久的收集器。特點:
單執行緒收集,且垃圾收集時,必須暫停其他所有的工作執行緒(Stop The World, STW),直到它收集結束。
HotSpot 虛擬機器執行在 Client 模式下預設新生代收集器。
優於其他收集器的地方:簡單而高效(與其他收集器的單執行緒比)。對於限定單個 CPU 的環境來說,Serial 收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。
執行示意圖如下:
3。2 ParNew 收集器
ParNew 收集器實質上是 Serial 收集器的多執行緒並行版本。
除了同時使用多條執行緒進行垃圾收集之外,其餘的行為包括 Serial 收集器可用的所有控制引數(-XX:SurvivorRatio, -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集演算法、Stop The World、物件分配原則、回收策略等都與 Serial 收集器完全一致。
執行示意圖:
使用/禁用該收集器的 VM 引數
# 注:JDK 9 取消了 -XX:+UseParNewGC 引數
-XX:+/-UseParNewGC
3。3 Parallel Scavenge 收集器
Parallel Scavenge 收集器是新生代收集器,也是使用標記-複製演算法實現的、並行收集的多執行緒收集器,也稱“吞吐量優先收集器”。
與 ParNew 類似,但關注點不同:
CMS 等收集器:儘可能地縮短垃圾收集時使用者執行緒的停頓時間;
Parallel Scavenge 收集器:達到一個可控的吞吐量(Throughput)。
吞吐量 = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間),即執行使用者程式碼時間所佔比重。
響應速度快能提升使用者體驗;而吞吐量高則能更高效地利用 CPU 資源,儘快完成程式的計算任務(主要適合在後臺運算而不需要太多互動的任務)。
執行示意圖如下:
虛擬機器引數
# 最大垃圾收集停頓時間(毫秒)
-XX:MaxGCPauseMillis
# 設定吞吐量(0~100)
-XX:GCTimeRatio
# 自適應調節策略
-XX:UseAdaptiveSizePolicy
3。4 Serial Old 收集器
Serial 收集器的老年代版本,單執行緒,使用“標記-整理”演算法。主要用於客戶端模式下的 HotSpot 虛擬機器。
執行示意圖如下:
3。5 Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本,支援多執行緒併發收集,使用多執行緒和“標記-整理”演算法實現。
執行示意圖如下:
在注重吞吐量或者處理器資源較為稀缺的場合,都可以考慮 Parallel Scavenge + Parallel Old 收集器的組合。
3。6 CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一種以「獲取最短回收停頓時間」為目標的收集器。
它基於“標記-清除”演算法實現,運作過程分為四步:
初步標記(CMS initial mark):只標記 GC Roots 能直接關聯到的物件,速度很快;
併發標記(CMS concurrent mark):從 GC Roots 遍歷整個物件圖,耗時較長,但無需停頓使用者執行緒(可與使用者執行緒併發執行);
重新標記(CMS remark):修正併發標記期間,因使用者執行緒導致標記產生變動的標記記錄;
併發清除(CMS concurrent sweep):清理刪除標記階段判斷的已經死亡的物件,可與使用者執行緒併發執行。
執行示意圖如下:
目前很大一部分 Java 應用集中在網際網路網站或者 B/S 系統的服務上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給使用者帶來較好的體驗。CMS 收集器非常符合這類應用的需求。
CMS 的主要優缺點
主要優點:併發收集、低停頓
主要缺點
對處理器資源非常敏感,降低吞吐量。
無法處理“浮動垃圾”,有可能出現“Concurrent Mode Failure”失敗而導致另一次完全 Stop The World 的 Full GC 的產生。
記憶體空間碎片問題
浮動垃圾(Floating Garbage):在 CMS 的併發標記和併發清理階段,使用者執行緒是還在繼續執行的,程式在執行自然就還會伴隨有新的垃圾物件不斷產生,但這一部分垃圾物件是出現在標記過程以後,CMS 無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉。這部分垃圾就是“浮動垃圾”。
虛擬機器引數
# 使用 CMS 收集器
-XX:+UseConcMarkSweepGC
# 老年代使用空間的比例(需根據實際情況權衡)
-XX:CMSInitiatingOccupancyFraction
=
80
# Full GC 時開啟記憶體碎片的合併整理
-XX:+UseCMSCompactAtFullCollection
3。7 Garbage First 收集器
Garbage First(G1) 收集器是垃圾收集器發展史上里程碑式的成果,開創了「面向區域性收集」的設計思路和「基於 Region」的記憶體佈局形式。
G1 收集器的定位是「CMS 收集器的替代者和繼承人」。由於實現較複雜,後文另行分析。這裡只做簡單描述:
JDK 7 Update 40 時,Oracle 認為它達到了足夠成熟的商用程度;
JDK 8 Update 40 時;G1 收集器提供了併發的類解除安裝支援,被 Oracle 稱為“全功能的垃圾收集器(Fully-Featured Garbage Collector)”。
此外,還有一些更為先進的低延遲收集器,比如 OracleJDK 11 加入的 ZGC,RedHat 公司的 Shenandoah 收集器。另外,還有一個有點“奇葩”的 Epsilon 收集器,等等。
衡量垃圾收集器優劣的指標主要有三個:記憶體佔用(Footprint)、吞吐量(Throughput)和延遲(Latency)。此三者構成了一個「三元悖論」(類似分散式系統中的 CAP 原則),難以同時滿足。
4。 小結
本文簡要介紹了一些垃圾收集的相關概念,常用的垃圾收集演算法以及經典的垃圾收集器。由於 G1 收集器實現稍複雜,因此後面單獨分析。本文主要內容概括如下圖: