【JVM】JVM垃圾收集器、垃圾收集算法、無用對象

Java 常見的垃圾收集器有哪些

實際上,垃圾收集器(GC,Garbage Collector)是和具體 JVM 實現緊密相關的,不一樣廠商(IBM、Oracle),不一樣版本的JVM,提供的選擇也不一樣。接下來,我來談談最主流的 Oracle JDK。java

  • 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 之間是複製算法,但總體上實際可看做是標記 - 整理(MarkCompact)算法,能夠有效地避免內存碎片,尤爲是當
Java 堆很是大的時候,G1 的優點更加明顯。less

G1 吞吐量和停頓表現都很是不錯,而且仍然在不斷地完善,與此同時 CMS 已經在 JDK 9 中被標記爲廢棄(deprecated),因此 G1 GC 值得你深刻掌握。性能

垃圾收集的原理和基礎概念

對象實例收集

自動垃圾收集的前提是清楚哪些內存能夠被釋放。即如何判斷一個對象是否能夠回收。
主要就是兩個方面,最主要部分就是對象實例,都是存儲在堆上的;還有就是方法區中的元數據等信息,例如類型再也不使用,卸載該 Java 相似乎是很合理的。測試

對於對象實例收集,主要是兩種基本算法,引用計數可達性分析雲計算

引用計數算法,顧名思義,就是爲對象添加一個引用計數,用於記錄對象被引用的狀況,若是計數爲0,即表示對象可回收。這是不少語言的資源回收選擇,例如因人工智能而更加火熱的Python,它更是同時支持引用計數和垃圾收集機制。具體哪一種最優是要看場景的,業界有大規模實踐中僅保留引用計數機制,以提升吞吐量的嘗試。
Java 並無選擇引用計數,是由於其存在一個基本的難題,也就是很難處理循環引用關係。

另外就是 Java 選擇的可達性分析,Java 的各類引用關係,在某種程度上,將可達性問題還進一步複雜化,這種類型的垃圾收集一般叫做追蹤性垃圾收集(Tracing Garbage Collection)。其原理簡單來講,就是將對象及其引用關係看做一個圖,選定活動的對象做爲 GC Roots,而後跟蹤引用鏈條,若是一個對象和 GC Roots 之間不可達,也就是不存在引用鏈條,那麼便可認爲是可回收對象。JVM 會把虛擬機棧和本地方法棧中正在引用的對象、靜態屬性引用的對象和常量,做爲 GC Roots。

方法區無用元數據的回收比較複雜,簡單梳理一下。通常來講初始化類加載器加載的類型是不會進行類卸載(unload)的;而普通的類型的卸載,每每是要
求相應自定義類加載器自己被回收,因此大量使用動態類型的場合,須要防止元數據區(或者早期的永久代)不會 OOM。在 8u40 之後的 JDK 中,下面參數已是默認的:

-XX:+ClassUnloadingWithConcurrentMark

常見的垃圾收集算法

主要分爲三類。

  • 複製(Copying)算法

我前面講到的新生代 GC,基本都是基於複製算法,將活着的對象複製到 to 區域,拷貝過程當中將對象順序放置,就能夠避免內存碎片化。

這麼作的代價是,既然要進行復制,既要提早預留內存空間,有必定的浪費;另外,對於 G1 這種分拆成爲大量 region 的 GC,複製而不是移動,意味着 GC 須要維護 region 之間對象引用關係,這個開銷也不小,無論是內存佔用或者時間開銷。

  • 標記 - 清除(Mark-Sweep)算法

首先進行標記工做,標識出全部要回收的對象,而後進行清除。這麼作除了標記、清除過程效率有限,另外就是不可避免的出現碎片化問題,這就致使其不適合特別大的堆;不然,一旦出現 Full GC,暫停時間可能根本沒法接受。

  • 標記 - 整理(Mark-Compact)

相似於標記 - 清除,但爲避免內存碎片化,它會在清理過程當中將對象移動,以確保移動後的對象佔用連續的內存空間。

注意,這些只是基本的算法思路,實際 GC 實現過程要複雜的多,目前還在發展中的前沿 GC 都是複合算法,而且並行和併發兼備。

若是對這方面的算法有興趣,能夠參考一本比較有意思的書《垃圾回收的算法與實現》,雖然其內容並非圍繞 Java 垃圾收集,可是對通用算法講解比較形象。

垃圾收集過程

第一,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 仍然處於飛速發展之中,目前的默認選項 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
    簡單說就是個不作垃圾收集的 GC,彷佛有點奇怪,有的狀況下,例如在進行
    性能測試的時候,可能須要明確判斷 GC 自己產生了多大的開銷,這就是其典型應用場景。

  • ZGC
    這是 Oracle 開源出來的一個超級 GC 實現,具有使人驚訝的擴展能力,好比支持 T bytes 級別的堆大小,而且保證絕大部分狀況下,延遲都不會超過 10ms。雖然目前還處於實驗階段,僅支持 Linux 64 位的平臺,但其已經表現出的能力和潛力都很是使人期待。

固然,其餘廠商也提供了各類獨具一格的 GC 實現,例如比較有名的低延遲 GC,Zing和Shenandoah等。

參考資料:
第27講 | Java常見的垃圾收集器有哪些?

相關文章
相關標籤/搜索