1。 一些概念

1。1 垃圾&垃圾收集

垃圾:在 JVM 語境下,“垃圾”指的是死亡的物件所佔據的堆空間。

垃圾收集:所謂“垃圾收集”,就是將已分配出去、但不再使用的記憶體回收回來,以便能再次分配。

1。2 物件是否死亡

如何判斷一個物件是否死亡(即不可能再被任何途徑使用)?通常有以下兩種方法:

1。2。1 引用計數法

引用計數法(Reference Counting):為每個物件新增一個引用計數器,用來統計指向該物件的引用個數。當有地方引用它時,計數器加一;引用失效時減一。當某個物件的引用計數為零時,說明該物件已死亡,便可以被回收了。

主要優點:原理簡單,判定效率高。

主要缺點:無法解決迴圈依賴的問題(物件之間相互迴圈引用)。

1。2。2 可達性分析演算法

可達性分析(Reachability Analysis)的基本思路如下:

透過一系列稱為 GC Roots 的根物件作為起始節點集,從這些節點開始,根據引用關係向下搜尋,搜尋過程所走過的路徑稱為“引用鏈”(Reference Chain),若某個物件到 GC Roots 間沒有任何引用鏈相連(或者用圖論的話來說就是從 GC Roots 到這個物件不可達時)則證明此物件是不可能再被使用的。

示意圖如下:

JVM筆記-垃圾收集演算法與垃圾收集器

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)演算法:最早、最基礎的演算法,分為“標記”和“清除”兩個階段:

標記出所有需要回收的物件(或反之);

在標記完成後統一回收所有被標記的物件(或反之)。

後續的收集演算法大多都是以“標記-清除”演算法為基礎,對其缺點進行改進而得。

該演算法的示意圖如下:

JVM筆記-垃圾收集演算法與垃圾收集器

優缺點分析:

主要優點:實現簡單;

主要缺點:

執行效率不穩定,標記和清除過程效率都不高(標記物件較多時);

記憶體空間碎片化問題(碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作)。

圖片來自:

http://www。

cnblogs。com/dolphin0520

/p/3783345。html

2。2 標記-複製演算法

簡稱複製(Copying)演算法。現在的商用 Java 虛擬機器大都優先採用了這種收集演算法回收新生代。

“半區複製”(Semispace Copying)演算法將可用記憶體按容量分為大小相等的兩塊,每次只使用其中的一塊。當這一塊用完時,就將還存活的物件複製到另外一塊上,然後再把已使用的記憶體空間一次清理掉。

該演算法主要為了解決“標記-清除”演算法面對大量可回收物件時執行效率低的問題。

示意圖如下:

JVM筆記-垃圾收集演算法與垃圾收集器

“半區複製”演算法的優缺點:

優點:實現簡單,執行高效且不易產生記憶體碎片;

缺點:可用記憶體縮小為原來的一半,空間浪費太多。

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)演算法:標記過程與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

標記-清除演算法與標記-整理演算法區別:前者是一種非移動式的回收演算法,後者是移動式的。

示意圖:

JVM筆記-垃圾收集演算法與垃圾收集器

主要問題:

移動存活物件:回收記憶體時會更復雜(Stop The World);

不移動存活物件:分配記憶體時會更復雜(空間碎片問題)。

2。4 分代收集演算法

當前商業虛擬機器的垃圾收集都採用“分代收集”(Generational Collection)演算法(不是一種具體的演算法實現,可以理解為「組合模式」):根據物件存活週期的不同,將記憶體劃分為幾塊。

一般把 Java 堆分為新生代和老年代,根據各個年代的特點採用最適當的收集演算法。

新生代

每次垃圾收集時,都有大批物件死去,只有少量存活,採用複製演算法(只需複製少量存活物件)。

老年代

物件存活率較高,沒有額外空間對它進行分配擔保,必須使用“標記-清除”或“標記-整理”演算法進行回收。

3。 垃圾收集器

前面的收集演算法只是記憶體回收的方法論,而垃圾收集器才是記憶體回收的具體實現(可理解為“介面”與“實現類”的關係)。

3。1 Serial 收集器

Serial 收集器是最基礎、歷史最悠久的收集器。特點:

單執行緒收集,且垃圾收集時,必須暫停其他所有的工作執行緒(Stop The World, STW),直到它收集結束。

HotSpot 虛擬機器執行在 Client 模式下預設新生代收集器。

優於其他收集器的地方:簡單而高效(與其他收集器的單執行緒比)。對於限定單個 CPU 的環境來說,Serial 收集器由於沒有執行緒互動的開銷,專心做垃圾收集自然可以獲得最高的單執行緒收集效率。

執行示意圖如下:

JVM筆記-垃圾收集演算法與垃圾收集器

3。2 ParNew 收集器

ParNew 收集器實質上是 Serial 收集器的多執行緒並行版本。

除了同時使用多條執行緒進行垃圾收集之外,其餘的行為包括 Serial 收集器可用的所有控制引數(-XX:SurvivorRatio, -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集演算法、Stop The World、物件分配原則、回收策略等都與 Serial 收集器完全一致。

執行示意圖:

JVM筆記-垃圾收集演算法與垃圾收集器

使用/禁用該收集器的 VM 引數

# 注:JDK 9 取消了 -XX:+UseParNewGC 引數

-XX:+/-UseParNewGC

3。3 Parallel Scavenge 收集器

Parallel Scavenge 收集器是新生代收集器,也是使用標記-複製演算法實現的、並行收集的多執行緒收集器,也稱“吞吐量優先收集器”。

與 ParNew 類似,但關注點不同:

CMS 等收集器:儘可能地縮短垃圾收集時使用者執行緒的停頓時間;

Parallel Scavenge 收集器:達到一個可控的吞吐量(Throughput)。

吞吐量 = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間),即執行使用者程式碼時間所佔比重。

響應速度快能提升使用者體驗;而吞吐量高則能更高效地利用 CPU 資源,儘快完成程式的計算任務(主要適合在後臺運算而不需要太多互動的任務)。

執行示意圖如下:

JVM筆記-垃圾收集演算法與垃圾收集器

虛擬機器引數

# 最大垃圾收集停頓時間(毫秒)

-XX:MaxGCPauseMillis

# 設定吞吐量(0~100)

-XX:GCTimeRatio

# 自適應調節策略

-XX:UseAdaptiveSizePolicy

3。4 Serial Old 收集器

Serial 收集器的老年代版本,單執行緒,使用“標記-整理”演算法。主要用於客戶端模式下的 HotSpot 虛擬機器。

執行示意圖如下:

JVM筆記-垃圾收集演算法與垃圾收集器

3。5 Parallel Old 收集器

Parallel Scavenge 收集器的老年代版本,支援多執行緒併發收集,使用多執行緒和“標記-整理”演算法實現。

執行示意圖如下:

JVM筆記-垃圾收集演算法與垃圾收集器

在注重吞吐量或者處理器資源較為稀缺的場合,都可以考慮 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):清理刪除標記階段判斷的已經死亡的物件,可與使用者執行緒併發執行。

執行示意圖如下:

JVM筆記-垃圾收集演算法與垃圾收集器

目前很大一部分 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 收集器實現稍複雜,因此後面單獨分析。本文主要內容概括如下圖:

JVM筆記-垃圾收集演算法與垃圾收集器

JVM筆記-垃圾收集演算法與垃圾收集器