一. JVM垃圾回收算法 1.引用計數器算法: 引用計數器算法是給每個對象設定一個計數器,當有地方引用這個對象的時候,計數器+1,當引用失效的時候,計數器-1,當計數器為0的時候,JVM就認為對象不再被使用,是“垃圾”了。 引用計數器實作簡單,效率高;但是 (1)不能解決循環引用問問題 (A對象引用B對象,B對象又引用A對象,但是A,B對象已不被任何其他對象引用),同時 (2)每次計數器的增加和減少都帶來了很多額外的開銷 ,是以在 JDK1.1 之後,這個算法已經不再使用了。
2.根搜尋方法 根搜尋方法是通過一些“GC Roots”對象作為起點,從這些節點開始往下搜尋,搜尋通過的路徑成為 引用鍊 (Reference Chain), 當一個對象沒有被GC Roots的引用鍊連接配接的時候,說明這個對象是不可用的。 GC Roots對象包括: a) 虛拟機棧(棧幀中的本地變量表)中的引用的對象。 b) 方法區域中的類靜态屬性引用的對象。 c) 方法區域中常量引用的對象。 d) 本地方法棧中JNI(Native方法)的引用的對象。 了解了JVM是怎麼确定對象是“垃圾”之後,進入正題,讓我們來看看垃圾回收的算法。
1.複制算法(Copying) 複制算法是 把記憶體分成大小相等的兩塊,每次使用其中一塊,當垃圾回收的時候,把存活的對象複制到另一塊上,然後把這塊記憶體整個清理掉。 複制算法 實作簡單 , 運作效率高 ,但是由于每次隻能使用其中的一半,造成記憶體的 使用率不高 。 現在的JVM用 複制方法收集 新生代 ,由于新生代中大部分對象(98%)都是朝生夕死的,是以兩塊記憶體預設大概是8:1。 垃圾回收前:

垃圾回收後:
2.标記—清除算法(Mark-Sweep) 标記—清除算法包括兩個階段:“标記”和“清除”。 在标記階段,确定所有要回收的對象,并做标記。 清除階段,将标記階段确定不可用的對象清除。 标記—清除算法是基礎的收集算法,标記和清除階段的 效率不高 ,而且清除後回 産生大量的不連續空間 , 這樣當程式需要配置設定大記憶體對象時,可能無法找到足夠的連續空間。 垃圾回收前:
垃圾回收後:
3.标記—整理算法(Mark-Compact) 标記—整理算法和标記—清除算法一樣,但是标記—整理算法不是把存活對象複制到另一塊記憶體,而是把存活對象往記憶體的一端移動,然後直接回收邊界以外的記憶體。 标記—整理算法 提高了記憶體的使用率 ,并且它适合在收集對象存活時間較長的 老年代 。 垃圾回收前:

垃圾回收後:
4.分代收集(Generational Collection) 分代收集是根據對象的存活時間把記憶體分為新生代和老年代,根據個代對象的存活特點,每個代采用不同的垃圾回收算法。 新生代 采用 複制算法,老年代 采用 标記—整理算法 (個人感覺也不一定,如果老年代采用 CMS收集器 ,算法就是 标記-清除 算法) 。 垃圾算法的實作涉及大量的程式細節,而且不同的虛拟機平台實作的方法也各不相同。上面介紹的隻不過是基本思想。
二. JVM垃圾收集器 垃圾收集器就是收集算法的具體實作,不同的虛拟機會提供不同的垃圾收集器。并且提供參數供使用者根據自己的應用特點和要求組合各個年代所使用的收集器。 本文讨論的收集器 基于Sun Hotspot虛拟機 1.6版。 下圖中展示了jdk1.6中提供的6種作用于不同年代的收集器,兩個收集器之間存在連線的話就說明它們可以搭配使用。沒有最好的收集器,也沒有萬能的收集器,隻有最合适的收集器。 從Serial收集器到Parallel收集器,再到CMS收集器, G1收集器, 使用者線程的停頓時間在不斷縮短,但是仍然沒有辦法完全消除。
 1. Serial收集器 單線程收集器,使用 複制收集算法, 收集時會暫停所有工作線程(我們将這件事情稱之為Stop The World),直到收集結束,虛拟機運作在Client模式時的預設新生代收集器。 收集過程: 暫停所有線程 算法: 複制算法 優點: 簡單高效(與其他收集器的單線程相比),對于限定單個CPU的環境來說,Serial收集器沒有現成互動的開銷。 應用: Client模式下的預設新生代收集器 場景: 在堆比較小的情況下,一般停頓時間很短,是可以使用這種收集器的。
新生代 Serial 與年老代 Serial Old 搭配垃圾收集過程圖:
2. ParNew收集器 ParNew收集器就是Serial的多線程版本,除了使用多條收集線程外,其餘行為包括算法、STW、對象配置設定規則、回收政策等都與Serial收集器一摸一樣。 ParNew收集器是許多運作在 server模式下的虛拟機中首選的新生代收集器(一個原因是在除了serial收集器外,目前隻有它能與CMS收集器配合使用)。 ParNew收集器是使用-XX:+UseConcMarkSweepGC選項的預設新生代收集器;也可以用-XX:+UseParNewGC選項來強制指定它。 ParNew收集器在單CPU環境中不比Serial效果好,甚至可能更差,兩個CPU也不一定跑的過,但随着CPU數量的增加,性能會逐漸增加。 預設開啟的收集線程數與CPU數量相同。在CPU數量很多的情況下,可以使用 -XX:ParallelGCThreads參數來限制線程數。 收集過程: 與使用者線程并發 算法: 複制算法 優點:在CPU多的情況下,擁有比Serial更好的效果。單CPU環境下Serial效果更好 應用: 許多運作在 Server模式下的虛拟機中首選的新生代收集器
3. Parallel Scavenge收集器 同ParNew一樣是使用複制算法的新生代并行多線程收集器。 Parallel Scavenge的特點是它的關注點與其他收集器不同, CMS等收集器 的關注點 盡可能地縮短垃圾收集時使用者線程的停頓時間 , 而Parallel Scavenge 收集器的目标則是 達到一個可控制的吞吐量, 也被稱為 吞吐量優先收集器 。 吞吐量=運作使用者代碼時間/(運作使用者代碼時間+垃圾收集時間)。 高吞吐量和停頓時間短的政策相比,主要強調任務更快完成,适用于背景運算而不需要太多互動的任務;而後者強調使用者互動體驗。
Parallel Scavenge提供兩個參數精确控制吞吐量, -XX:MaxGCPauseMillis 控制最大垃圾收集停頓時間 和-XX:GCTimeRatio 設定吞吐量大小 1).MaxGCPauseMillis允許的值是一個大于零0的毫秒數, 收集器将盡力保證記憶體回收花費的時間不超過設定值 。 GC停頓時間縮小 是以 犧牲吞吐量 和 新生代空間 來換取的,也就是要使停頓時間更短,需要使新生代的空間減小,這樣垃圾回收的頻率會增加,吞吐量也降下來了。 2).GCTimeRatio的值是一個大于0小于100的整數,也就是 垃圾收集時間占總時間的比率 。 預設為 99 ,則允許最大GC時間就占總時間的1/(1+99). 3). -XX:+UseAdaptiveSizePolicy, 打開GC自适應調節政策 後 會自動設定新生代大小、調整Eden與Survior區的比例、晉升老年代對象年齡,新生代大小等細節參數。 這個參數也是Parallel Scavenge和ParNew的重要差別。
算法 : 複制算法 應用: 适合在背景運算而不需要太多互動的任務
新生代 ParNew / Parallel Scavenge 與年老代 Serial Old 搭配垃圾收集過程圖:
4. Serial Old收集器 是Serial收集器的老年代版本,也同樣是一個 單線程 的收集器,使用 标記-整理 算法。主要是client模式下的虛拟機使用。參考上面圖Serial/Serial old. 兩大用途 : (1) 在 JDK1.5及之前 的版本中與Parallel Scavenge搭配使用 ; (2) 作為CMS收集器的後備預案, 在并發收集發生 Concurrent Mode Failure 時使用 。 收集過程: 暫停所有使用者線程, 單線程 算法: 标記-整理算法 應用:主要意義是 Client模式下的收集器 ,如果在Server模式下:參看上面的兩大用途。
5. Parallel Old收集器 是Parallel Scavenge收集器的老年代版本, 在 JDK1.6中才開始使用 。 由于之前的版本中, Parallel Scavenge 隻有使用Serial Old作為老年代收集器,其吞吐量優先的設計思路不能被很好的貫徹. 在 Parallel Old 收集器出現後, Parallel Scavenge 和 Parallel Old 的配合主要用于貫徹這種 吞吐量優先的設計思路 。 收集過程: 多線程 算法: 标記-整理算法 應用: 在注重吞吐量及CPU資源敏感的場合,可以優先考慮Parallel Scavenge加Parallel Old收集器
新生代 Parallel Scavenge 和年老代 Parallel Old 收集器搭配運作過程圖:
6. CMS收集器 Concurrent Mark Sweep 以擷取最短回收停頓時間為目标的收集器,比較理想的應用場景是B/S架構的伺服器。 CMS收集器工作過程:
基于 标記-清除算法 實作,運作過程分成4個步驟: a)初始标記(需要stop the world),标記一下GC Roots能直接關聯到的對象,速度很快 b)并發标記,進行GC Roots Tracing的過程。 c)重新标記(需要stop the world),為了修正并發标記時使用者繼續運作而産生的标記變化,停頓時間比初始标記長,遠比并發标記短。 d)并發清除 缺點: 1). CMS收集器對CPU資源非常敏感 。在并發階段,它雖然不會導緻使用者線程停頓,但是因為占用了一部分CPU資源而導緻應用程式變慢,總吞吐量就會降低。 CMS預設啟動的回收線程數為 (CPU數量+3)/4 。為了解決這一情況,有一個變種i-CMS,但目前并不推薦使用。 2) .CMS收集器 無法處理浮動垃圾(floating garbage),浮動垃圾即在并發清除階段因為是并發執行,還會産生垃圾,這一部分垃圾即為浮動垃圾,要等下次收集。。 同樣由于 CMS GC階段使用者線程還需要運作 ,即還需要預留足夠的記憶體空間供使用者線程使用,是以CMS收集器 需要預留一部分空間提供并發收集時的程式運作使用 。 預設設定下,CMS收集器在 老年代使用了68%的空間 後就會被激活 。 這個值可以用 -XX:CMSInitiatingOccupancyFraction 來設定。 要是CMS運作期間 預留的記憶體無法滿足程式需要 ,就會出現 concurrent mode failure ,這時候就會啟用 Serial Old 收集器作為備用進行老年代的垃圾收集 。 3). 空間碎片過多(标記-清除算法的弊端),提供-XX:+UseCMSCompactAtFullCollection參數,應用于在FULL GC後再進行一個碎片整理過程; -XX:CMSFullGCsBeforeCompaction,多少次不壓縮的full gc後來一次帶壓縮的。
7. G1收集器 (jdk1.7後全新的回收器, 用于取代CMS) HotSpot開發團隊賦予它的使命是未來可以替換掉JDK1.5中釋出的CMS收集器。其與其它收集器相比,G1具備如下特點:
- 并行與并發:和CMS類似。
- 分代收集:分代概念在G1中依然得以保留。雖然G1可以不需要其它收集器配合就能獨立管理整個GC堆,但它能夠采用不同的方式去處理新建立的對象和已經存活了一段時間、熬過多次GC的舊對象以擷取更好的收集效果。也就是說G1可以自己管理新生代和老年代了。
- 空間整合:由于G1使用了獨立區域(Region)概念,G1從整體來看是基于“标記-整理”算法實作收集,從局部(兩個Region)上來看是基于“複制”算法實作的,但無論如何,這兩種算法都意味着G1運作期間不會産生記憶體空間碎片。
- 可預測的停頓:這是G1相對于CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用這明确指定一個長度為M毫秒的時間片段内,消耗在垃圾收集上的時間不得超過N毫秒。
-XX:+UseG1GC:使用 G1 回收器。 G1收集器與前面的CMS收集器相比有兩個顯著的改進: 一是G1收集器是基于 “ 标記-整理 ” 算法實作的收集器,也就是說它不會産生空間碎片,這對于長時間運作的應用系統來說非常重要。 二是它可以 非常精确地控制停頓 , 即既能讓使用者明确指定在一個長度為M毫秒的時間片段内,消耗在垃圾收集上的時間不得超過N毫秒 ,這幾乎已經是實時Java(RTSJ)的垃圾收集器的特征了。 G1收集器可以 實作 在基本不犧牲吞吐量的前提下完成低停頓的記憶體回收 ,這是由于它能夠極力地避免全區域的垃圾收集,之前的收集器進行收集的範圍都是整個新生代或老年代,而G1 将 整個Java堆劃分為多個大小固定的獨立區域 (Region),并且 跟蹤 這些區域裡面的 垃圾堆積程度 ,在背景維護一個 優先清單 ,每次根據允許的收集時間, 優先回收垃圾最多的區域 (這就是Garbage First名稱的來由) 。 區域劃分及有優先級的區域回收 ,保證了G1收集器在有限的時間内可以獲得最高的收集效率 。 在G1收集器中,Region之間的對象引用以及其他收集器中的新生代與老年代之間的對象引用,虛拟機都是使用 Remembered Set 來 避免全堆掃描 。 G1中每個Region都有一個與之對應的Remembered Set,虛拟機發現程式 對Reference類型資料進行寫操作時 ,會産生一個 Write Barrier 暫時 中斷寫操作 , 檢查 Reference引用的對象 是否處于不同的Region之間 (在分代中例子中就是檢查是否老年代中的對象引用了新生代的對象), 如果是便通過 CardTable 把相關引用資訊記錄到 被引用對象所屬的Region的Remembered Set中 。 當記憶體回收時,在GC根節點的枚舉範圍加入Remembered Set即可保證不對全局堆掃描也不會有遺漏。 如果不計算維護Remembered Set的操作,G1收集器的運作大緻可劃分為以下幾個步驟:
- 初始标記(Initial Making)
- 并發标記(Concurrent Marking)
- 最終标記(Final Marking)
- 篩選回收(Live Data Counting and Evacuation)
看上去跟CMS收集器的運作過程有幾分相似,不過确實也這樣。 初始階段僅僅隻是标記一下GC Roots能直接關聯到的對象,并且修改TAMS(Next Top Mark Start)的值,讓下一階段使用者程式并發運作時,能在正确可以用的Region中建立新對象,這個階段需要停頓線程,但耗時很短。 并發标記階段是從GC Roots開始對堆中對象進行可達性分析,找出存活對象,這一階段耗時較長但能與使用者線程并發運作。 而 最終标記 階段需要 把 Remembered Set Logs 的資料合并到 Remembered Set 中,這階段需要停頓線程,但可并行執行 。 最後 篩選回收 階段 首先對 各個Region的回收價值和成本進行排序 ,根據使用者所期望的GC停頓時間來制定回收計劃 ,這一過程同樣是需要停頓線程的,但Sun公司透露這個階段其實也可以做到并發,但考慮到停頓線程将大幅度提高收集效率,是以選擇停頓。下圖為G1收集器運作示意圖:
總結 傳統的GC收集器将連續的記憶體空間劃分為 新生代 、 老年代 和 永久代 ( JDK 8去除了永久代,引入了元空間Metaspace ),這種劃分的特點是 各代的存儲位址是連續的 。
而G1的 各代存儲位址是不連續的 ,每一代都使用了 n個不連續的大小相同的Region , 每個Region占有一塊連續的虛拟記憶體位址 。