引言
垃圾回收(GC,Garbage Collection)
在筆者上一篇文章中(JVM內存模型),介紹了JVM內存模型以及JVM運行時的數據區,堆是JVM內存區域里面最大的一塊區域,用於存放實例數據,因此這一塊區域是垃圾回收的重點區域,而堆為了提高垃圾回收效率,又被分為了年輕代和老年代,年輕代又被分為了eden區、survivor區。

基礎概念
判斷垃圾
接下來我們就討論Jvm是怎么回收堆這部分內存的。在進行回收前垃圾收集器第一件事情就是確定哪些對象還存活,哪些已經死去。下面介紹兩種基礎的回收算法(找垃圾)。
引用計數法
給對象添加一個引用計數器,每當有一個地方引用它時計數器就+1,當引用失效時計數器就-1,。只要計數器等於0的對象就是不可能再被使用的。
此算法在大部分情況下都是一個不錯的選擇,也有一些著名的應用案例(據說python使用的是此算法),但是Java虛擬機中是沒有使用的。
優點:實現簡單、判斷效率高。
缺點:當幾個對象存在互相循環引用,但這幾個對象組成了一個圓環,沒有任何對象指向這個圓環了,所以這個整體應該被回收,但它的引用計算不等於0,造成無法進行回收
Object a = new Object();
Object b = new Object();
a=b;
b=a;
a=b=null; //這樣就導致gc無法回收他們。
根可達算法
虛擬機規定一個GC ROOT標准,當從這些GC ROOT往下引用查找的時候,能夠引用得到,則不是垃圾,如果從任何GC ROOT都無法引用到某一個對象,則這個對象,就會標記為垃圾
主流的商用程序語言(Java、C#等)在主流的實現中,都是通過可達性分析來判定對象是否存活的。

圖中Object1-5不是垃圾,Object6/7/8會被認為是垃圾
在Java語言中,可作為GC Roots 的對象包括下面幾種
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區中靜態變量引用的對象
- 方法區中常量引用的對象
- 本地方法棧(即一般說的 Native 方法)中JNI引用的對象
GC分代
為了使 JVM 能夠更好的管理堆內存中的對象,包括內存的分配以及回收。
堆的內存模型大致為:

Java 中的堆也是 GC 收集垃圾的主要區域。GC 分為兩種:Minor GC(或稱為 young GC)、Full GC ( 或稱為 Major GC )。
新生代
新生代幾乎是所有 Java 對象出生的地方,即 Java 對象申請的內存以及存放都是在這個地方。Java 中的大部分對象通常不需長久存活,具有朝生夕滅的性質。(對於大對象,直接進入放在老年代)
對象優先在新生代 Eden 區中分配,如果 Eden 區沒有足夠的空間時,就會觸發一次 Minor GC 。
當對象在 Eden ( 包括一個 Survivor 區域,這里假設是 from 區域 ) 出生后,在經過一次 Minor GC 后,如果對象還存活,並且能夠被另外一塊 Survivor 區域所容納( 上面已經假設為 from 區域,這里應為 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用復制算法將這些仍然還存活的對象復制到另外一塊 Survivor 區域 ( 即 to 區域 ) 中,然后清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),並且將這些對象的年齡設置為1,以后對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1( 對象的當前GC年齡存在對象的headr中的),當對象的年齡達到某個值時 (默認是 15 歲,可以通過參數 -XX:MaxTenuringThreshold 來設定 ),這些對象就會成為老年代。
老生代
當對象經歷過規定次數的Minor GC后,如果還有幸存活,則晉升至老年代(或者一些比較大的對象,一出生就會在老年代)。
現實的生活中,老年代的人通常會比新生代的人 "早死"。堆內存中的老年代(Old)不同於這個,老年代里面的對象幾乎個個都是在 Survivor 區域中熬過來的,它們是不會那么容易就 "死掉" 了的。因此,Full GC 發生的次數不會有 Minor GC 那么頻繁,並且做一次 Full GC 要比進行一次 Minor GC 的時間更長。FULL GC采用的是標記清除(或整理)算法
新生代與老年代的關系圖

垃圾回收算法
復制(Copy)
將內存按容量划分為兩塊,每次只使用其中一塊。當這一塊內存用完了,就將存活的對象復制到另一塊上,然后再把已使用的內存空間一次清理掉。這樣使得每次都是對半個內存區回收,也不用考慮內存碎片問題,簡單高效。缺點需要兩倍的內存空間。
應用場景:回收新生代;如Serial收集器、ParNew收集器、Parallel Scavenge收集器、G1(從局部看)
復制算法執行過程如下:

- 優點:實現簡單,效率高。解決了標記-清除算法導致的內存碎片問題。
- 缺點:代價太大,將內存縮小了一半。效率隨對象的存活率升高而降低。
標記—清除(Mark-Sweep)
GC分為兩個階段,標記和清除。首先標記所有可回收的對象,在標記完成后統一回收所有被標記的對象。同時會產生不連續的內存碎片。碎片過多會導致以后程序運行時需要分配較大對象時,無法找到足夠的連續內存,而不得已再次觸發GC。
應用場景:針對老年代的CMS收集器
1. 標記
一次標記:在經過可達性分析算法后,對象沒有與GC Root相關的引用鏈,那么則被第一次標記。並且進行一次篩選:當對象有必要執行finalize()方法時,則把該對象放入F-Queue隊列中。
二次標記:對F-Queue隊列中的對象進行二次標記。在執行finalize()方法時,如果對象重新與GC Root引用鏈上的任意對象建立了關聯,則把他移除出“ 即將回收 ”集合。否則就等着被回收吧!!!
對被第一次標記切被第二次標記的,就可以判定位可回收對象了
2. 清除
兩次標記后,還在“ 即將回收 ”集合的對象進行回收。
執行過程如下:

- 優點:基礎最基礎的可達性算法,后續的收集算法都是基於這種思想實現的
- 缺點:標記和清除效率不高,產生大量不連續的內存碎片,導致創建大對象時找不到連續的空間,不得不提前觸發另一次的垃圾回收。
標記—整理(Mark-Compact)
標記-整理算法是根據老年代的特點應運而生。
也分為兩個階段,首先標記可回收的對象,再將存活的對象都向一端移動,然后清理掉邊界以外的內存。此方法避免標記-清除算法的碎片問題,同時也避免了復制算法的空間問題。
應用場景:很多垃圾收集器采用這種算法來回收老年代,如Serial Old收集器、G1(從整體看)

- 優點:不會像復制算法那樣隨着存活對象的升高而降低效率,不像標記-清除算法那樣產生不連續的內存碎片
- 缺點:效率問題,除了像標記-清除算法的標記過程外,還多了一步整理過程,效率更低。
一般年輕代中執行GC后,會有少量的對象存活,就會選用復制算法,只要付出少量的存活對象復制成本就可以完成收集。而老年代中因為對象存活率高,沒有額外過多內存空間分配,就需要使用標記-清理或者標記-整理算法來進行回收。
垃圾回收器
根據堆中不同分代的特征,在JVM的歷史長河中,誕生了各種各樣的收集器,下面我們就以常見的幾種做一些基本的認識

在很早以前,計算機內在只有幾十M的時候,串行收集器基本上能滿足使用,但隨着硬件性能不斷提高,內存大小和CPU運行速度的提升,在JVM發展的不同時期,誕生了針對當時計算機性能的垃圾回收器
在G1以前,物理和邏輯上都進行了分代,即將堆分為年輕代和老年代,直到G1的出現,這種分代概念就愈發模糊了,因為G1收集器針對的是整堆的收集。
基本概念
STW:Stop The World,當垃圾回收線程工作時,需要暫停當前的用戶(業務)線程,這個過程稱它為STW;
串行收集:單線程收集器,簡單高效,因為是單線程的原因,也就不會產生用戶態和內核態切換所帶來的開銷;(會產生 STW )
並行收集:隨着內存地不斷增大,單CPU實現了多核的技術,通過多線程的方式收集垃圾可以極大地提升效率;(會產生 STW )
並發收集:指用戶線程與垃圾收集線程同時工作(不一定是並行的可能會是交替執行)。用戶程序在繼續運行,而垃圾收集程序運行在另一個CPU上(多核CPU)(並發標記 和 並發清除 階段不會產生STW)
Serial 收集器
串行收集器,它是最早誕生的垃圾回收器,以單線程的方式進行垃圾收集,在JVM剛出來的情況下,計算機內存與現在相比特別地小,即便是串行回收,它的速度依然很快。
特點:單線程、簡單高效(與其他收集器的單線程相比),對於限定單個CPU的環境來說,Serial收集器由於沒有線程交互的開銷,專心做垃圾收集自然可以獲得最高的單線程手機效率。收集器進行垃圾回收時,必須暫停其他所有的工作線程,直到它結束(Stop The World)。
應用場景:小內存、單核CPU情況下的垃圾收集
Serial / Serial Old收集器運行示意圖

ParNew 收集器
ParNew收集器其實就是Serial收集器的多線程版本,除了使用多線程外其余行為均和Serial收集器一模一樣(參數控制、收集算法、Stop The World、對象分配規則、回收策略等)
特點:多線程、ParNew收集器默認開啟的收集線程數與CPU的數量相同,在CPU非常多的環境中,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
和Serial收集器一樣存在Stop The World問題
應用場景:ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器,因為它是除了Serial收集器外,唯一一個能與CMS收集器配合工作的。
ParNew 收集器運行示意圖

Parallel Scavenge 收集器
收集與吞吐量關系密切,故也稱為吞吐量優先收集器。
特點:屬於新生代收集器也是采用復制算法的收集器,又是並行的多線程收集器(與ParNew收集器類似)。
該收集器的目標是達到一個可控制的吞吐量。還有一個值得關注的點是:GC自適應調節策略(與ParNew收集器最重要的一個區別)
GC自適應調節策略:Parallel Scavenge收集器可設置-XX:+UseAdptiveSizePolicy參數。當開關打開時不需要手動指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代的對象年齡(-XX:PretenureSizeThreshold)等,虛擬機會根據系統的運行狀況收集性能監控信息,動態設置這些參數以提供最優的停頓時間和最高的吞吐量,這種調節方式稱為GC的自適應調節策略。
Parallel Scavenge收集器使用兩個參數控制吞吐量:
XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時間
XX:GCRatio 直接設置吞吐量的大小。
Serial Old 收集器
Serial Old是Serial收集器的老年代版本。
特點:同樣是單線程收集器,采用標記-整理算法。
應用場景:主要也是使用在Client模式下的虛擬機中。也可在Server模式下使用。
它在Server模式下主要的兩大用途:
- 在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用。
- 作為CMS收集器的后備方案,在並發收集Concurent Mode Failure時使用。
Serial / Serial Old收集器工作過程圖(Serial收集器圖示相同):

Parallel Old 收集器
它是Parallel Scavenge收集器的老年代版本。
特點:多線程,采用標記-整理算法。
應用場景:注重高吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old (PS + PO,JDK1.8默認) 收集器。
Parallel Scavenge/Parallel Old收集器工作過程圖:

CMS 收集器
一種以獲取最短回收停頓時間為目標的收集器,用於老年代的垃圾回收
特點:基於標記—清除算法實現,與用戶線程並發收集、並發清除,低停頓、低延時
應用場景:適用於注重服務的響應速度,希望系統停頓時間最短,給用戶帶來更好的體驗等場景下。如web程序、b/s服務。
CMS收集器的運行過程可以大致分為以下四個階段
初始標記
標記老年代中的所有GC Roots對象
標記年輕代中活着的對象引用到老年代的對象(指的是年輕帶中還存活的引用類型對象,引用指向老年代中的對象)
並發標記
進行GC Roots Tracing 的過程,找出存活對象且與用戶線程可並發執行。
從“初始標記”階段標記的對象開始找出所有存活的對象
因為是並發執行,在用戶線程運行的時候,會發生新生代對象晉升到老年代、或者是更新老年代對象的引用關系等等,對於這些新生成或改變的引用關系,可能會存在漏標,所有就必須要進行下一階段的“重新標記”,為了提高下了階段重新標記的效率,該階段會把上述對象所在的Card標識為Dirty,下一階段只需掃描這些Dirty Card的對象,避免掃描整個老年代;
並發標記階段只負責將引用發生改變的Card標記為Dirty狀態,不負責處理。
由於這個階段是和用戶線程並發執行的,可能會導致concurrent mode failure
重新標記
為了修正並發標記期間因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄。仍然存在Stop The World問題。
由於之前的預處理階段是與用戶線程並發執行的,這時候可能年輕帶的對象對老年代的引用已經發生了很多改變,這個時候,remark階段要花很多時間處理這些改變,會導致很長stop the word,所以通常CMS盡量運行Final Remark階段在年輕代是足夠干凈的時候。
另外,還可以開啟並行收集:-XX:+CMSParallelRemarkEnabled,提高reMark效率
並發清理
對標記的對象進行清除回收。
通過以上5個階段的標記,老年代所有存活的對象已經被標記並且現在要通過Garbage Collector采用清掃的方式回收那些不能用的對象了。
這個階段主要是清除那些沒有標記的對象並且回收空間;
由於CMS並發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之后,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱為“浮動垃圾”。
CMS收集器的工作流程圖

G1 收集器
G1是一款面向服務端應用的垃圾收集器
它是在CMS的基礎上改進而來,現已被JDK1.9作為默認的垃圾回收器
特點如下:
並行與並發:G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU來縮短Stop-The-World停頓時間。部分收集器原本需要停頓Java線程來執行GC動作,G1收集器仍然可以通過並發的方式讓Java程序繼續運行。
分代收集:G1能夠獨自管理整個Java堆,並且采用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。
空間整合:G1運作期間不會產生空間碎片,收集后能提供規整的可用內存。
可預測的停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型。能讓使用者明確指定在一個長度為M毫秒的時間段內,消耗在垃圾收集上的時間不得超過N毫秒。、
G1為什么能建立可預測的停頓時間模型?
因為它有計划的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region里面的垃圾堆積的大小,在后台維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。這樣就保證了在有限的時間內可以獲取盡可能高的收集效率。
G1與其他收集器的區別:
其他收集器的工作范圍是整個新生代或者老年代、G1收集器的工作范圍是整個Java堆。在使用G1收集器時,它將整個Java堆划分為多個大小相等的獨立區域(Region)。雖然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔離的,他們都是一部分Region(不需要連續)的集合。
G1收集器存在的問題:
Region不可能是孤立的,分配在Region中的對象可以與Java堆中的任意對象發生引用關系。在采用可達性分析算法來判斷對象是否存活時,得掃描整個Java堆才能保證准確性。其他收集器也存在這種問題(G1更加突出而已)。會導致Minor GC效率下降。
G1收集器是如何解決上述問題的?
采用Remembered Set來避免整堆掃描。G1中每個Region都有一個與之對應的Remembered Set,虛擬機發現程序在對Reference類型進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用對象是否處於多個Region中(即檢查老年代中是否引用了新生代中的對象),如果是,便通過CardTable把相關引用信息記錄到被引用對象所屬的Region的Remembered Set中。當進行內存回收時,在GC根節點的枚舉范圍中加入Remembered Set即可保證不對全堆進行掃描也不會有遺漏。
如果不計算維護 Remembered Set 的操作,G1收集器大致可分為如下步驟:
初始標記
僅標記GC Roots能直接到的對象,並且修改TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序並發運行時,能在正確可用的Region中創建新對象。(需要線程停頓,但耗時很短。)
並發標記
從GC Roots開始對堆中對象進行可達性分析,找出存活對象。(耗時較長,但可與用戶程序並發執行)
最終標記
為了修正在並發標記期間因用戶程序執行而導致標記產生變化的那一部分標記記錄。且對象的變化記錄在線程Remembered Set Logs里面,把Remembered Set Logs里面的數據合並到Remembered Set中。(需要線程停頓,但可並行執行。)
篩選回收
對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計划。(可並發執行)
G1收集器運行流程圖

