一、概述
1. 哪些記憶體需要回收
上篇文章 我們介紹了 Java 記憶體運作時區域的各個部分,其中程式計數器、虛拟機棧、本地方法棧三個區域随線程而生,随線程而滅,在這幾個區域内就不需要過多考慮回收的問題,因為方法結束或者線程結束時,記憶體自然就跟着回收了。
而方法區和 Java 堆是線程共享的,我們隻有在程式處于運作期間才能知道會建立哪些對象,這部分記憶體的配置設定和回收都是動态的,垃圾收集器所關注的是這部分記憶體。
2. 回收方法區
方法區的垃圾收集主要回收兩部分内容:廢棄常量和無用的類。
“廢棄常量”指的是目前系統中沒有任何一個對象引用指向該常量。
“無用的類”需要同時滿足下面三個條件才有可能被虛拟機回收,至于最終是否回收還由虛拟機參數:-Xnoclassgc 控制。
- 該類的所有執行個體都已被回收,也就是 Java 堆中不存在該類的任何執行個體。
- 加載該類的 ClassLoader 已經被回收。
- 該類對應的 Class 對象沒有被任何地方被引用,無法在任何地方通過反射通路該類的接口。
二、經典垃圾回收器
首先開始之前先看下 HotSpot 虛拟機所包含的收集器:

圖中展示了7種作用于不同分代的收集器,如果兩個收集器之間存在連線,則說明它們可以搭配使用。虛拟機所處的區域則表示它是屬于新生代還是老年代收集器。
1. Serial 收集器
新生代收集器,複制算法收集,Serial 收集器是最基本、發展曆史最悠久的收集器。它是一個單線程的收集器,隻會使用一個 CPU 或一條收集線程去完成垃圾收集工作,它在垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束。
簡單高效;虛拟機 Client 模式下表現優異(Client 模式下記憶體較小、CPU較少,能減少許多線程互動的開銷)。
缺點:回收工作需要 Stop The World ;單線程;不适用虛拟機 Server 模式(Server 模式下記憶體較大、CPU較多,導緻回收工作停頓時間過長)。
2. ParNew 收集器
新生代收集器,複制算法收集,ParNew 收集器其實就是 Serial 收集器的多線程版本,除了使用多線程進行回收外,其餘行為包括控制參數、收集算法、Stop The World、對象配置設定規則、回收政策等都與 Serial 收集器完全一樣。
多線程工作;可以與 CMS 收集器搭配工作;虛拟機 Server 模式下表現優異。
回收工作需要 Stop The World 。
3. Parallel Scavenge 收集器
新生代收集器,複制算法收集,多線程工作,Parallel Scavenge 收集器的關注點在于達到一個可控制的吞吐量(其他收集器的關注點是縮短垃圾收集時使用者線程的停頓時間),停頓時間越短越适合需要與使用者互動的程式;而高吞吐量則可以高效率的利用 CPU 時間,盡快完成程式的運作任務。
GC 自适應調節政策是 Parallel Scavenge 收集器和 ParNew 收集器的一個重要差別。它變現為:隻需要把基本的記憶體資料設定好(如 -Xmx 設定最大堆),然後使用 MaxGCPauseMillis 參數(更關注最大停頓時間)或 GCRatio(更關注吞吐量)給虛拟機設立一個優化目标,那具體細節參數的調節工作就由虛拟機來完成了。
多線程工作;注重系統吞吐量和CPU資源;自适應調節政策。
回收工作需要 Stop The World ;可選的老年代收集器過少,無法與 CMS 收集器配合工作,在 JDK1.5 之前隻能和 Serial Old 收集器配合工作。
tips:- 吞吐量 = 運作使用者代碼時間 / (運作使用者代碼時間 + 垃圾收集時間)
- 自适應調節政策使用 -XX:+UseAdptiveSizePolicy 參數打開。
- 與吞吐量關系密切,故也稱為“吞吐量優先”收集器。
4. Serial Old 收集器
老年代收集器,标記-整理算法,單線程,Serial Old 收集器是 Serial 收集器的老年代版本。
虛拟機 Client 模式下表現尚可(Client 模式下記憶體較小、CPU較少,能減少許多線程互動的開銷);CMS 收集器的後備預案(在并發收集Concurent Mode Failure時使用)。
回收工作需要 Stop The World ;單線程。
5. Parallel Old 收集器
老年代收集器,标記-整理算法,多線程,Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本,在 JDK1.6 後開始提供。
搭配 Parallel Scavenge 收集器使用,關注系統吞吐量以及CPU資源。
回收工作需要 Stop The World ;可搭配的新生代收集器僅有 Parallel Scavenge 收集器而已。
6. CMS 收集器
老年代收集器,标記-清除算法,多線程,CMS(Concurrent Mark Sweep)收集器是一種以獲得最短回收停頓時間為目标的收集器,是真正意義上與使用者線程并發運作的收集器,是以,使用 CMS 收集器能給使用者帶來良好的體驗。
- 并發收集;低停頓。
- 對于大概 4GB 到 6GB 以下的堆記憶體,CMS 一般處理的比較好。
- CMS 收集器對 CPU 資源敏感,在并發标記/清理 的時候,雖然不會導緻使用者線程停頓,但标記/清理工作是要占用一部分 CPU 資源的,這無疑會降低吞吐量。(CMS 預設啟動的回收線程數是 (CPU 數量 + 3)/ 4)
- CMS 收集器無法處理浮動垃圾(Floating Garbage),可能出現 “Concurent Mode Failure” 失敗而導緻另一次 Full GC 的産生(使用 Serial Old 收集器)。浮動垃圾指的是并發清理階段,使用者線程并發運作産生的垃圾,當這些浮動垃圾的記憶體超過了CMS 運作期間預留的記憶體,就會導緻 “Concurent Mode Failure” 失敗。
- CMS 收集器使用的标記-清除算法會有大量的記憶體碎片出現,将會給大對象配置設定帶來很多麻煩。
CMS 收集器的一些好文章:
- JVM 垃圾回收器CMS之各階段總結
- JVM調優實戰:解決CMS concurrent-abortable-preclean LongGC的問題
7. G1 收集器
分區(Region)收集器,标記-整理算法和複制算法,多線程,是一款面向服務端應用的垃圾收集器,它的目标也是獲得最短停頓時間 —— 消耗在垃圾收集上的時間大機率不超過 N 毫秒。
G1(Garbage-First)收集器在 JDK 7u4 版本釋出,它的期望是未來可以替換 CMS 收集器。終于在 JDK9 中 G1 收集器成為了預設的垃圾收集器,而 CMS 則成為了不推薦使用的收集器。
雖然 G1 收集器仍是遵循分代理論設計的,但其堆記憶體的布局與其他收集器有非常明顯的差異:G1 不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的 Java 堆劃分為多個大小相等的獨立區域(Region),每一個 Region 都可以根據需要,扮演新生代的 Eden 空間、Survivor空間,或者老年代空間。G1 可以面對任何 Region 來組成回收集(Collection Set,一般簡稱為 CSet)進行回收,衡量标準不再是它屬于哪個分代,而是哪塊記憶體中存放的垃圾數量最多,回收收益最大。進而優先處理回收價值收益最大的那些 Region,這也就是 “Garbage First”名字的由來。
- 并行和并發,縮短 Stop The World 停頓的時間。
- 标記-整理算法、複制算法不會出現類似 CMS 的記憶體碎片問題。
- 可預測的停頓時間模型(-XX:G1HeapRegionSize 設定,預設是 200 ms),能讓使用者明确指定在一個長度為 M 毫秒的時間片段内,消耗在垃圾收集上的時間不超過 N 毫秒。
G1的首要目的是為那些需要大容量記憶體和較小 GC 延遲的應用程式提供解決方案。這通常是指那些堆大小設定在 6GB 以上,确定的、可以預測的暫停時間在 0.5 秒以内的應用程式。
如果應用程式符合以下一項或者多項特征,那麼從 CMS 或者 ParallelOld 收集器切換到 G1 可能更合适。
- 活動對象占據了超過 50% 的 Java 堆空間。
- 對象配置設定率或者提升率波動明顯。
- 不希望有長時間的垃圾收集暫停時間(超過0.5秒或1秒)。
三、低延遲垃圾收集器
從 G1 開始,最先進的垃圾收集器的設計導向都不約而同地變為追求能夠應付應用的記憶體配置設定速率,而不追求一次把整個 Java 堆全部清理幹淨。這樣,應用在配置設定,同時收集器在收集,隻要收集的速度能夠跟得上對象配置設定的速度,那一切就能運作的很完美。
衡量垃圾收集器的三項最重要的名額是:記憶體占用(Footprint)、吞吐量(Throughput)和延遲(Latency),三者共同構成了一個“不可能三角”。随着計算機硬體的發展,“記憶體占用”和“吞吐量”的缺陷可以由硬體來彌補,是以“延遲”成為了垃圾收集器最被重視的性能名額。
ZGC(JDK11)、Shenandoah(OpenJDK12) 收集器,幾乎整個工作過程全部都是并發的,隻有初始标記、最終标記這些階段有短暫的停頓,這部分停頓的時間基本上是固定的,與堆的容量、堆中對象的數量沒有正比例關系(可以在任意可管理的堆容量下,實作垃圾收集的停頓不超過10ms),這兩款目前仍處于實驗狀态的收集器,被官方命名為“低延遲垃圾收集器”。
1. Shenandoah 收集器
Shenandoah 收集器,标記-整理算法和複制算法,多線程, 與 G1 類似也使用 Region 的堆記憶體布局,也使用 Humongous Region 存放大對象,預設的回收政策也同樣是優先回收價值最大的 Region。
Shenandoah 的回收階段可以與使用者線程并發,這是 Shenandoah 的核心功能,而 G1 收集器無法做到。
Shenandoah 預設不使用分代收集算法,不區分新生代和老年代。
Shenandoah 摒棄了在 G1 中耗費大量記憶體和計算資源去維護的記憶集,改用名為“連接配接矩陣”(Connection Matrix)的全局資料結構來記錄跨 Region 的引用關系,降低了處理跨代指針時的記憶集維護消耗,也降低了僞共享問題的發生機率。
Shenandoah 收集階段大抵可以了解為三個并發階段(并發标記、并發回收、并發引用更新),其中并發回收是最核心的階段,基于标記-整理算法來并發回收是很複雜的,其困難點是在移動對象的同時,使用者線程仍然可能不停對被移動的對象進行讀寫通路,很難一瞬間全部改變過來。 Shenandoah 采取的方式是讀屏障和“Brooks Pointers”的轉發指針,包括後面的并發引用更新,也是為了解決這個事宜。
2. ZGC 收集器
ZGC 收集器,标記-整理算法和複制算法,多線程,基于 Region 記憶體布局,不設分代的,使用了讀屏障、染色指針和記憶體多重映射等技術來實作可并發的标記-整理算法,以低延遲為首要目标。
ZGC 收集器中 Region 具有動态性 —— 動态建立和銷毀以及動态的區域容量大小。在 x64 硬體平台上,ZGC 的 Region 分為大、中、小三類容量。
ZGC 每次回收都會掃描所有的 Region,用範圍更大的掃描成本換取省去 G1 中記憶集的維護成本。
四、另類的收集器 —— Epsilon
Epsilon 收集器出現在 JDK11 中,這是一款以不能夠進行垃圾收集為“賣點”的垃圾收集器。
近年來大型系統從傳統單體應用向微服務化、無服務化方向發展的趨勢已越發明顯,傳統 Java 有着記憶體占用較大,在容器中啟動時間長,即時編譯需要緩慢優化等特點,這對大型應用來說并不是什麼太大的問題,但對短時間、小規模的服務形式就有諸多不适。為了應對新的技術潮流,最近幾個版本的 JDK 逐漸加入了提前編譯、面向應用的類資料共享等支援。Epsilon 收集器也有着類似的目标,如果應用隻要運作數分鐘甚至數秒,隻要Java虛拟機能正确配置設定記憶體,在堆耗盡之前就會退出,那顯然運作負載極小、沒有任何回收行為的 Epsilon 便是恰當的選擇。
參考連結:
- 《深入了解 JVM 虛拟機》
- G1垃圾收集器介紹
- jvm垃圾收集器(終結篇)