垃圾回收簡介
Java 會對內存進行自動分配與回收管理,使上層業務更加安全,方便地使用內存實現程序邏輯。在不同的 JVM 實現及不同的回收機制中,堆內存的划分方式是不一樣的。
簡要地介紹下垃圾回收(Garbage Collection,GC)。垃圾回收的主要目的是清除掉沒有引用/不再使用的對象,自動釋放內存。在了解垃圾回收算法之前,首先我們先要理解對象是怎么定義可以用被回收的。
引用計數算法
那么,GC 判斷對象可以回收的依據是什么呢?有一種判斷對象是否存活的算法是引用計數算法,該算法的原理是:給每一個對象分配一個引用計數器,每當有一個地方引用它時,計數器值就 +1 ;當引用失效時,計數器值就 -1 ;所以當對象的計數器值為 0 時,就可以判定該對象是可以被回收。引用計數法實現起來相對比較簡單,判定邏輯也不復雜。但是主流的 Java 虛擬機里面並沒有選用引用計數法來管理內存,因為該算法有個很大的痛點就是難以解決對象之間的循環引用。舉個例子,現在有兩個對象 objA 和 objB 都聲明了 instance 字段,代碼如下
Object objA = new Object();
Object objB = new Object();
...
objA.instance = objB;
objB.instance = objA;
除此之外,objA 和 objB 沒有任何的引用,也就是說這兩個對象除了彼此之外,再也不會被訪問,但就是因為它們倆互相引用着對方,導致它們的引用計數器不可能為0,引用計數算法也無法通知 GC 將這倆對象進行回收。
可達性分析算法
所以目前主流虛擬機采用最多的回收算法是可達性分析算法來判斷對象是否可以被回收,在 Java、C# 中都有大量的實現場景,JVM 也正是為了判斷對象存活,引入了GC Roots
,下面簡要地介紹該算法的思想:通過一系列的稱為“ GC Roots ”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到 GC Roots 沒有任何引用的鏈相連時(等同於對象與 GC Roots 之間沒有直接或間接的引用關系),則可證明此對象是不可用的,可以通知 GC 收集器回收。
那么什么對象可以作為GC Roots
呢?比如:類靜態屬性中引用的對象、常量引用的對象、虛擬機棧中引用的對象、本地方法棧中引用的對象等等都可以充當 GC Roots 的角色。下面通過繪圖的形式,更好地理解可達性分析算法的思想,對象 object 5、object 6、object7 雖然互相有關聯,但是它們到 GC Roots 是不可達的,所有被判定為可回收對象。
垃圾回收算法
前面我們了解了如何去判斷對象是否存活,下面我們認識下垃圾回收算法的基本思想。
標記-清除算法
算法思想
:該算法分為兩個階段,分別是標記
和清除
階段,從每個 GC Roots 開始,依次標記有引用關系的對象,最后將沒有標記的對象清除。
該算法主要兩點不足之處,一個是效率問題,無論是標記
還是清除
,他們的效率都不是很高;另一個是空間問題,這種算法會帶來大量的空間碎片,如果程序在運行過程當中,產生了一個很大的對象,需要較大的連續空間來分配該對象時,往往會出現老年代還有很大內存空間剩余,但是卻無法找到足夠的連續內存空間,不得已去觸發另一次垃圾收集動作(FGC)。
標記-整理算法
算法思想
:標記過程跟標記-清除
算法一樣,然后將存活的對象集中整理到內存空間的一端,形成一片連續的已使用的區域,最后再將該區域外的對象全部清除,這樣就避免了連續碎片的問題。
復制算法(Mark-Copy)
算法思想
:為了能夠並行地標記和整理,將可用內存按容量划分成大小相等的兩塊,每次只激活其中一塊。這樣,當其中一塊的內存用完了,垃圾回收時只需把存活的對象復制到另一塊未被激活的空間上,最后在清除掉除了未激活空間之外,其他占用內存空間的對象全部清除。
比如將Java堆內存空間分為較大的 Eden
和兩塊較小的 Survivor
,每次只使用 Eden 區和 Survivor 區其中的一塊,當垃圾回收時,就將 Eden 和 Survivor 區中存活的對象復制到另一塊未被使用的 Survivor 區,再清除掉 Eden 和用過的一塊 Survivor 區空間。HotSpot 虛擬機默認 Eden 和 Survivor 的大小比例是 8:1
,也就是每次新生代中可用內存空間為整個新生代容量的 90%,只有 10% 的內存會被浪費。復制算法現在就作為 YGC 算法進行新生代的垃圾回收。
這樣做的好處是每次只需要對整個空間一半的區域塊進行內存回收,內存分配時也就不用考慮內存碎片、較少了內存空間的浪費等復雜情況,只要移動堆頂指針,按順序分配內存即可。
分代收集算法
這種算法並沒有新的思想,只是根據對象存活周期的不同將內存划分成幾塊。一般是 Java 堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就采用復制算法
,只需要將存活的對象復制到未被使用的區域塊,效率很高;老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記-清除
或標記-整理
算法進行回收。
垃圾回收器
垃圾回收器(Garbage Collector,GC)是實現垃圾回收算法並應用在 JVM 環境中的內存管理模塊。
Serial 回收器/Serial Old 回收器
Serial 回收器是早期(JDK1.3.1 之前)虛擬機新生代回收的唯一選擇,是一個主要應用於 YGC
的垃圾回收器,采用的垃圾回收算法是標記-整理
算法,通過串行單線程的方式完成任務,串行就意味着每次只會使用一個 CPU 或一條回收線程去完成垃圾回收工作,並且在進行垃圾回收時,不允許其他線程與它一起工作,必須要停掉其他所有的工作線程,直至收集結束。這種情況就稱為:“Stop The World” 簡稱 STW
,即垃圾回收的某個階段會暫停整個應用程序的運行
。Serial 回收流程圖如下:
FGC 的執行時間較長,如果頻繁引起 FGC 會嚴重影響應用程序的性能。此外,還有一種回收器叫 Serial Old回收器 ,它是 Serial 回收器的老年代版本,所以它也一樣是單線程回收器,采用的也是標記-整理
算法。
即使是這樣,與其他回收器的單線程比,Serial 回收器也是有着優於它們的地方,對於限定單個 CPU 的環境來說,Serial 回收器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程回收效率。不論是 Serial 還是 Serial Old 回收器,它們的主要意義是在於給 Client 模式下的虛擬機使用。
CMS回收器
CMS(Concurrent Mark Sweep)回收器是一種以獲取最短回收停頓時間為目標,是目前比較流行的垃圾回收器。對於Java 編程語言實現互聯網或者 B/S 系統的服務端,並且十分重視服務的響應速度,希望停頓時間越短越好,方便給予用戶更好的使用體驗,采取 CMS 回收器的策略就十分符合這種應用場景。
CMS 回收器是基於標記-清除
算法實現的,整個垃圾回收工作步驟分為4個步驟:
- 初始標記(CMS initial mark)
- 並發標記(CMS concurrent mark)
- 重新標記(CMS remark)
- 並發清除(CMS concurrent sweep)
對於1、3步驟,也就是初始標記
和重新標記
階段還是會引發 STW(Stop The World),而2、4步驟的並發標記
和並發清除
兩個階段可以和應用程序並發執行,所以也屬於比較耗時的操作,但是無須擔心 CMS 回收器會影響到應用程序的正常運行。
初始標記階段僅僅只是標記一下 GC Roots 能直接關聯到的對象,速度很快;並發標記階段就是進行 GC Roots Tracing 的過程;重新標記階段是為了修正並發標記
期間,因用戶程序繼續運行而可能會導致標記產生變動的那一部分對象,進行標記記錄,這一階段的停頓時間一般會比初始標記階段長點,但不會比並發標記階段的時間長。
在垃圾回收的4個步驟中,並發標記
和並發清除
過程中所耗時最長,並且它們是可以跟用戶的線程在同一時間工作,所以從時間上來看,CMS 回收器的內存回收過程和用戶線程是一起並發執行的。CMS回收流程圖大致如下:
所以 CMS 回收器是一款十分優秀的收集器,有着並發收集、低停頓的優點,所以也稱為並發低停頓收集器,盡管如此,CMS 還是存在不足之處:
-
CMS 回收器對CPU資源十分敏感。雖然說,在並發階段,CMS 回收器可以跟用戶線程並發執行,但還是會占用一部分的 CPU 資源,從而導致應用程序響應變慢,系統壓力過高,導致系統最終的吞吐量降低。
-
CMS 回收器無法處理浮動垃圾,可能會導致出現 “Concurrent Mode Failure” 失敗而導致另一次 FGC 的產生。
-
CMS 回收器執行完垃圾回收后,會產生大量的空間碎片。這是由於 CMS 回收器采取的
標記-清除
算法所帶來的影響(具體可以往上看標記-清除算法部分)。為了解決這一問題,CMS 回收器可以通過配置-XX:+UseCMSCompactAtFullCollection
開關參數(默認是開啟的)。用於在 CMS 回收器頂不住要進行 FGC 的時候,開啟內存碎片的合並整理過程,解決了空間碎片問題,但由於空間整理期間是無法並發
的,無法並發就會引起 STW 的情況。但是好在 CMS 回收器的設計者為了減少STW次數,允許通過配置-XX:+CMSFullGCsBeforeCompaction=n
參數,該參數 n 意味着,在執行了 n 次 FGC 之后,JVM 才能在老年代執行空間碎片整理;參數默認值為0
,則表示每次執行完 FGC 之后,都要進行空間碎片整理。
G1回收器
Hotspot 在 JDK7 中推出了新一代 G1
(Garbage-First)垃圾回收,通過 -XX:+UseG1GC
參數啟用。在 JDK11 中,已經把 G1 設為默認垃圾回收器,可通過 jstat
命令查看垃圾回收情況。和 CMS
相比,G1
具備壓縮功能,能避免碎片問題。並且 G1 的暫停問題更加可控,總體上性能還是很不錯的。
在 G1 之前,其他回收器進行垃圾收集時,收集的范圍都是整個新生代或老年代
,而 G1 是 將 Java 堆空間分割成了若干相同大小的獨立區域,即 region ,其中包括 Eden 、Survivor 、Old 、Humongous 四種類型。其中, Humongous
是特殊的 Old 類型,專門放置大型對象
。圖中可以看出,新生代和老年代不再是物理隔離,它們都是一部分 Region(不再連續)的集合。
G1 回收器之所以能夠建立可預測的停頓
時間模型,是因為它可以有計划地避免在整個 Java 堆中進行全區域的垃圾收集
。G1 跟蹤各個 Region 里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需要的時間),在后台維護起一個優先級別列表
;所以它每次只需要根據允許收集的時間,優先收集價值回收率最高的 Region(這也是 Garbage-First 名稱的由來)。這種使用 Region 方式划分成若干份大小相同的內存空間,以及有優先級別根據的區域回收方式,保證了G1 回收器在有限的時間內提高了回收效率。
與其他的 GC 回收器比,G1 回收器有着以下的特點:
- 並行與並發:G1 能充分利用
多核
、多CPU
環境下的硬件優勢,使用多個CPU來縮短 STW 停頓的時間;部分回收器需要停頓其他 Java 線程執行的 GC 動作,而 G1 回收器可以與 Java 程序並發執行。 - 回收算法:G1 采用的是
Mark-Copy
(復制算法),有很好的空間整合能力,在 G1 執行期間不會產生大量的空間碎片,並且回收完成之后能夠提供規整的可用內存,有利於程序長時間運行。 - 可預測的停頓:能夠盡可能快地在指定時間內完成垃圾回收任務,能夠讓使用者明確指定在一個長度在 M 毫秒的時間片段內,消耗在垃圾收集上的時間不能超過 N 毫秒(M > N)。
參考資料《深入理解Java虛擬機》、《碼出高效》