垃圾收集機制是 Java 的招牌能力,極大地提高了開發效率。如今,垃圾收集幾乎成為現代語言的標配,即使經過如此長時間的發展, Java 的垃圾收集機制仍然在不斷的演進中,不同大小的設備、不同特征的應用場景,對垃圾收集提出了新的挑戰,這當然也是面試的熱點。
今天我要問你的問題是,Java 常見的垃圾收集器有哪些?
## 典型回答
實際上,垃圾收集器(GC,Garbage Collector)是和具體 JVM 實現緊密相關的,不同廠商(IBM、Oracle),不同版本的 JVM,提供的選擇也不同。接下來,我來談談最主流的 Oracle JDK。
* Serial GC,它是最古老的垃圾收集器,“Serial”體現在其收集工作是單線程的,并且在進行垃圾收集過程中,會進入臭名昭著的“Stop-The-World”狀態。當然,其單線程設計也意味著精簡的 GC 實現,無需維護復雜的數據結構,初始化也簡單,所以一直是 Client 模式下 JVM 的默認選項。
從年代的角度,通常將其老年代實現單獨稱作 Serial Old,它采用了標記 - 整理(Mark-Compact)算法,區別于新生代的復制算法。
Serial GC 的對應 JVM 參數是:
~~~
-XX:+UseSerialGC
~~~
* ParNew GC,很明顯是個新生代 GC 實現,它實際是 Serial GC 的多線程版本,最常見的應用場景是配合老年代的 CMS GC 工作,下面是對應參數
~~~
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
~~~
* CMS(Concurrent Mark Sweep) GC,基于標記 - 清除(Mark-Sweep)算法,設計目標是盡量減少停頓時間,這一點對于 Web 等反應時間敏感的應用非常重要,一直到今天,仍然有很多系統使用 CMS GC。但是,CMS 采用的標記 - 清除算法,存在著內存碎片化問題,所以難以避免在長時間運行等情況下發生 full GC,導致惡劣的停頓。另外,既然強調了并發(Concurrent),CMS 會占用更多 CPU 資源,并和用戶線程爭搶。
* Parrallel GC,在早期 JDK 8 等版本中,它是 server 模式 JVM 的默認 GC 選擇,也被稱作是吞吐量優先的 GC。它的算法和 Serial GC 比較相似,盡管實現要復雜的多,其特點是新生代和老年代 GC 都是并行進行的,在常見的服務器環境中更加高效。
開啟選項是:
~~~
-XX:+UseParallelGC
~~~
另外,Parallel GC 引入了開發者友好的配置項,我們可以直接設置暫停時間或吞吐量等目標,JVM 會自動進行適應性調整,例如下面參數:
~~~
-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N // GC 時間和用戶時間比例 = 1 / (N+1)
~~~
* G1 GC 這是一種兼顧吞吐量和停頓時間的 GC 實現,是 Oracle JDK 9 以后的默認 GC 選項。G1 可以直觀的設定停頓時間的目標,相比于 CMS GC,G1 未必能做到 CMS 在最好情況下的延時停頓,但是最差情況要好很多。
G1 GC 仍然存在著年代的概念,但是其內存結構并不是簡單的條帶式劃分,而是類似棋盤的一個個 region。Region 之間是復制算法,但整體上實際可看作是標記 - 整理(Mark-Compact)算法,可以有效地避免內存碎片,尤其是當 Java 堆非常大的時候,G1 的優勢更加明顯。
G1 吞吐量和停頓表現都非常不錯,并且仍然在不斷地完善,與此同時 CMS 已經在 JDK 9 中被標記為廢棄(deprecated),所以 G1 GC 值得你深入掌握。
## 考點分析
今天的問題是考察你對 GC 的了解,GC 是 Java 程序員的面試常見題目,但是并不是每個人都有機會或者必要對 JVM、GC 進行深入了解,我前面的總結是為不熟悉這部分內容的同學提供一個整體的印象。
對于垃圾收集,面試官可以循序漸進從理論、實踐各種角度深入,也未必是要求面試者什么都懂。但如果你懂得原理,一定會成為面試中的加分項。在今天的講解中,我側重介紹比較通用、基礎性的部分:
* 垃圾收集的算法有哪些?如何判斷一個對象是否可以回收?
* 垃圾收集器工作的基本流程。
另外,Java 一直處于非常迅速的發展之中,在最新的 JDK 實現中,還有多種新的 GC,我會在最后補充,除了前面提到的垃圾收集器,看看還有哪些值得關注的選擇。
## 知識擴展
**垃圾收集的原理和基礎概念**
第一,自動垃圾收集的前提是清楚哪些內存可以被釋放。這一點可以結合我前面對 Java 類加載和內存結構的分析,來思考一下。
主要就是兩個方面,最主要部分就是對象實例,都是存儲在堆上的;還有就是方法區中的元數據等信息,例如類型不再使用,卸載該 Java 類似乎是很合理的。
對于對象實例收集,主要是兩種基本算法,[引用計數](https://zh.wikipedia.org/wiki/%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0)和可達性分析。
* 引用計數算法,顧名思義,就是為對象添加一個引用計數,用于記錄對象被引用的情況,如果計數為 0,即表示對象可回收。這是很多語言的資源回收選擇,例如因人工智能而更加火熱的 Python,它更是同時支持引用計數和垃圾收集機制。具體哪種最優是要看場景的,業界有大規模實踐中僅保留引用計數機制,以提高吞吐量的嘗試。
Java 并沒有選擇引用計數,是因為其存在一個基本的難題,也就是很難處理循環引用關系。
* 另外就是 Java 選擇的可達性分析,Java 的各種引用關系,在某種程度上,將可達性問題還進一步復雜化,具體請參考[專欄第 4 講](http://time.geekbang.org/column/article/6970),這種類型的垃圾收集通常叫作追蹤性垃圾收集([Tracing Garbage Collection](https://en.wikipedia.org/wiki/Tracing_garbage_collection))。其原理簡單來說,就是將對象及其引用關系看作一個圖,選定活動的對象作為 GC Roots,然后跟蹤引用鏈條,如果一個對象和 GC Roots 之間不可達,也就是不存在引用鏈條,那么即可認為是可回收對象。JVM 會把虛擬機棧和本地方法棧中正在引用的對象、靜態屬性引用的對象和常量,作為 GC Roots。
方法區無用元數據的回收比較復雜,我簡單梳理一下。還記得我對類加載器的分類吧,一般來說初始化類加載器加載的類型是不會進行類卸載(unload)的;而普通的類型的卸載,往往是要求相應自定義類加載器本身被回收,所以大量使用動態類型的場合,需要防止元數據區(或者早期的永久代)不會 OOM。在 8u40 以后的 JDK 中,下面參數已經是默認的:
~~~
-XX:+ClassUnloadingWithConcurrentMark
~~~
第二,常見的垃圾收集算法,我認為總體上有個了解,理解相應的原理和優缺點,就已經足夠了,其主要分為三類:
* 復制(Copying)算法,我前面講到的新生代 GC,基本都是基于復制算法,過程就如[專欄上一講](http://time.geekbang.org/column/article/10325)所介紹的,將活著的對象復制到 to 區域,拷貝過程中將對象順序放置,就可以避免內存碎片化。
這么做的代價是,既然要進行復制,既要提前預留內存空間,有一定的浪費;另外,對于 G1 這種分拆成為大量 region 的 GC,復制而不是移動,意味著 GC 需要維護 region 之間對象引用關系,這個開銷也不小,不管是內存占用或者時間開銷。
* 標記 - 清除(Mark-Sweep)算法,首先進行標記工作,標識出所有要回收的對象,然后進行清除。這么做除了標記、清除過程效率有限,另外就是不可避免的出現碎片化問題,這就導致其不適合特別大的堆;否則,一旦出現 Full GC,暫停時間可能根本無法接受。
* 標記 - 整理(Mark-Compact),類似于標記 - 清除,但為避免內存碎片化,它會在清理過程中將對象移動,以確保移動后的對象占用連續的內存空間。
注意,這些只是基本的算法思路,實際 GC 實現過程要復雜的多,目前還在發展中的前沿 GC 都是復合算法,并且并行和并發兼備。
如果對這方面的算法有興趣,可以參考一本比較有意思的書《垃圾回收的算法與實現》,雖然其內容并不是圍繞 Java 垃圾收集,但是對通用算法講解比較形象。
**垃圾收集過程的理解**
我在[專欄上一講](http://time.geekbang.org/column/article/10325)對堆結構進行了比較詳細的劃分,在垃圾收集的過程,對應到 Eden、Survivor、Tenured 等區域會發生什么變化呢?
這實際上取決于具體的 GC 方式,先來熟悉一下通常的垃圾收集流程,我畫了一系列示意圖,希望能有助于你理解清楚這個過程。
第一,Java 應用不斷創建對象,通常都是分配在 Eden 區域,當其空間占用達到一定閾值時,觸發 minor GC。仍然被引用的對象(綠色方塊)存活下來,被復制到 JVM 選擇的 Survivor 區域,而沒有被引用的對象(黃色方塊)則被回收。注意,我給存活對象標記了“數字 1”,這是為了表明對象的存活時間。

第二, 經過一次 Minor GC,Eden 就會空閑下來,直到再次達到 Minor GC 觸發條件,這時候,另外一個 Survivor 區域則會成為 to 區域,Eden 區域的存活對象和 From 區域對象,都會被復制到 to 區域,并且存活的年齡計數會被加 1。

第三, 類似第二步的過程會發生很多次,直到有對象年齡計數達到閾值,這時候就會發生所謂的晉升(Promotion)過程,如下圖所示,超過閾值的對象會被晉升到老年代。這個閾值是可以通過參數指定:
~~~
-XX:MaxTenuringThreshold=<N>
~~~

后面就是老年代 GC,具體取決于選擇的 GC 選項,對應不同的算法。下面是一個簡單標記 - 整理算法過程示意圖,老年代中的無用對象被清除后, GC 會將對象進行整理,以防止內存碎片化。

通常我們把老年代 GC 叫作 Major GC,將對整個堆進行的清理叫作 Full GC,但是這個也沒有那么絕對,因為不同的老年代 GC 算法其實表現差異很大,例如 CMS,“concurrent”就體現在清理工作是與工作線程一起并發運行的。
**GC 的新發展**
GC 仍然處于飛速發展之中,目前的默認選項 G1 GC 在不斷的進行改進,很多我們原來認為的缺點,例如串行的 Full GC、Card Table 掃描的低效等,都已經被大幅改進,例如, JDK 10 以后,Full GC 已經是并行運行,在很多場景下,其表現還略優于 Parallel GC 的并行 Full GC 實現。
即使是 Serial GC,雖然比較古老,但是簡單的設計和實現未必就是過時的,它本身的開銷,不管是 GC 相關數據結構的開銷,還是線程的開銷,都是非常小的,所以隨著云計算的興起,在 Serverless 等新的應用場景下,Serial GC 找到了新的舞臺。
比較不幸的是 CMS GC,因為其算法的理論缺陷等原因,雖然現在還有非常大的用戶群體,但是已經被標記為廢棄,如果沒有組織主動承擔 CMS 的維護,很有可能會在未來版本移除。
如果你有關注目前尚處于開發中的 JDK 11,你會發現,JDK 又增加了兩種全新的 GC 方式,分別是:
* [Epsilon GC](http://openjdk.java.net/jeps/318),簡單說就是個不做垃圾收集的 GC,似乎有點奇怪,有的情況下,例如在進行性能測試的時候,可能需要明確判斷 GC 本身產生了多大的開銷,這就是其典型應用場景。
* [](http://openjdk.java.net/jeps/333)[ZGC](http://openjdk.java.net/jeps/333),這是 Oracle 開源出來的一個超級 GC 實現,具備令人驚訝的擴展能力,比如支持 T bytes 級別的堆大小,并且保證絕大部分情況下,延遲都不會超過 10 ms。雖然目前還處于實驗階段,僅支持 Linux 64 位的平臺,但其已經表現出的能力和潛力都非常令人期待。
當然,其他廠商也提供了各種獨具一格的 GC 實現,例如比較有名的低延遲 GC,[Zing](https://www.infoq.com/articles/azul_gc_in_detail)和[Shenandoah](https://wiki.openjdk.java.net/display/shenandoah/Main)等,有興趣請參考我提供的鏈接。
今天,作為 GC 系列的第一講,我從整體上梳理了目前的主流 GC 實現,包括基本原理和算法,并結合我前面介紹過的內存結構,對簡要的垃圾收集過程進行了介紹,希望能夠對你的相關實踐有所幫助。
## 一課一練
關于今天我們討論的題目你做到心中有數了嗎?今天談了一堆的理論,思考一個實踐中的問題,你通常使用什么參數去打開 GC 日志呢?還會額外添加哪些選項?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?