深入理解java虛擬機—— 超詳細虛擬機垃圾收集器原理,GC 垃圾收集器算法的深度解析,項目中如何選擇合適的垃圾收集器

 

目錄

一、收集器的功能

1、哪些內存需要回收?

2、什麼時候回收?

3、如何回收?

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

複製算法算法

標記-整理算法

分代收集算法

二、垃圾收集器有那些

併發垃圾收集和並行垃圾收集的區別

Minor GC和Major GC的區別

虛擬機經典回收器,7大垃圾回收器:

1、Serial 收集器

        2、ParNew收集器

        3、Parallel Scavenge收集器

        4、Serial Old收集器

        5、Parallel Old收集器

        6、CMS收集器

        7、G1收集器

G1內存佈局:


 

 

先看一張圖片:

有7中收集器  -- Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1。

 

(Garbage Collection,GC),大部分人都把這項技術當做Java語言的伴生產物。事實上,GC的歷史遠比Java久遠,1960年誕生於MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當List還在胚胎時期時,人們就在思考GC需要完成的3件事情:

雖然目前動態分配與內存回收技術已經相當成熟,一切看起來都進入了“自動化”時代,但只有了其內在,才能讓我們寫好每一句代碼。接下來就以上這三個問題逐一揭曉。

 

一、收集器的功能

 

1、哪些內存需要回收?

從JVM區域結構看,可將這些區域劃分爲“靜態內存”和“動態內存”兩類。程序計數器、虛擬機棧、本地方法3個區域是“靜態”的,因爲這幾個區域的內存分配和回收都具備確定性,都隨着線程而生,隨着線程而滅。但Java堆和方法區不一樣,內存分配都存在不確定性,只有在程序處於運行期間才能知道會創建哪些對象,這部分內存和回收都是動態的,垃圾收集器所關注的是這部分內存。

“靜態”區域的內存分配和回收已經很明確了,但“動態”區域到底是如何確定哪些內存該回收,哪些內存不該回收呢?我們都知道,Java是一種面嚮對象語言,在“動態”內存區域堆中的內存分配都是以“對象”爲單位存在(方法區後面再說)。如果要回收,肯定是回收哪些沒有用的對象了,那麼又怎麼確定對象已經沒用呢?請參考  我的上篇文章    虛擬機垃圾收集器最新垃圾回收器 如何回收何時回收對象

 

2、什麼時候回收?

判斷對象是否存活一般有兩種方式:

引用計數:每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數爲0時可以回收。此方法簡單,無法解決對象相互循環引用的問題。

可達性分析(Reachability Analysis):從GC Roots開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。不可達對象。

 

3、如何回收?

垃圾收集器是回收以上描述無用對象的具體實現,而垃圾收集算法又是垃圾收集器的方法論。所以,在介紹垃圾收集器之前,先了解一下這些垃圾收集算法,有助於理解後續的垃圾收集器的實現。

 

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

標記-清除(Mark-Sweep)算法是一種基礎的收集算法。

算法思路

       "標記-清除"算法,分爲兩個階段:

1、標記-清除

標記:

        首先標記出所有需要回收的對象;

(1)、第一次標記

       在可達性分析後發現對象到GC Roots沒有任何引用鏈相連時,被第一次標記;

       並且進行一次篩選:此對象是否必要執行finalize()方法;

       對有必要執行finalize()方法的對象,被放入F-Queue隊列中;    

(2)、第二次標記

       GC將對F-Queue隊列中的對象進行第二次小規模標記;

       在其finalize()方法中重新與引用鏈上任何一個對象建立關聯,第二次標記時會將其移出"即將回收"的集合;

       對第一次被標記,且第二次還被標記(如果需要,但沒有移出"即將回收"的集合),就可以認爲對象已死,可以進行回收。


詳細步驟參考《 虛擬機垃圾收集器最新垃圾回收器 如何回收何時回收對象

清除:

   兩次標記後,還在"即將回收"集合的對象將被統一回收;

 

2、優點

       基於最基礎的可達性分析算法,它是最基礎的收集算法;

       而後續的收集算法都是基於這種思路並對其不足進行改進得到的;

3、缺點

       主要有兩個缺點:

(1)、效率問題

       標記和清除兩個過程的效率都不高;

(2)、空間問題

       標記清除後會產生大量不連續的內存碎片;

       這會導致分配大內存對象時,無法找到足夠的連續內存;

       從而需要提前觸發另一次垃圾收集動作;
 

4、應用場景

      針對 老年代 的 CMS 收集器;

 

標記過程如下:

 

標記——清除過程圖:

 

  • 複製算法算法

 "複製"(Copying)收集算法,爲了解決標記-清除算法的效率問題;

1、算法思路

       (A)、把內存劃分爲大小相等的兩塊(待改良使用),每次只使用其中一塊;

       (B)、當一塊內存用完了,就將還存活的對象複製到另一塊上(而後使用這一塊);

       (C)、再把已使用過的那塊內存空間一次清理掉,而後重複步驟2;   

2、優點

       這使得每次都是隻對整個半區進行內存回收;

       內存分配時也不用考慮內存碎片等問題(可使用"指針碰撞"的方式分配內存);

      實現簡單,運行高效;

       (關於"指針碰撞"請參考  虛擬機爲新生對象分配內存的兩種分方式 、《Java對象在HotSpot虛擬機中的創建過程》)

3、缺點

(A)、空間浪費

      可用內存縮減爲原來的一半,太過浪費(解決:可以改良,不按1:1比例劃分);

(B)、效率隨對象存活率升高而變低(存活對象越多,複製效率越低,需要複製對象變多)

      當對象存活率較高時,需要進行較多複製操作,效率將會變低(解決:後面的標記-整理算法);

4、應用場景

      現在商業JVM都採用這種算法(通過改良缺點 (A) )來回收新生代;

            現在的商業虛擬機都採用這種收集算法來回收新生代,有企業分析的得出其實並不需求將內存按1:1的比例劃分,因爲新生代              中的對象大部分都是“朝生夕死”的。所以,HotSpot虛擬機默認的Eden和Survivor的大小比例是8:1。一塊Eden和兩塊                          Survivor,每次使用一塊Eden和一塊Survivor,也就是說只有10%是浪費的。如果另一塊Survivor都無法存放上次垃圾回收的              對象時,那這些對象將通過“擔保機制”進入老年代了。

      如Serial收集器、ParNew收集器、Parallel Scavenge收集器、、G1(從局部看);

複製算法執行過程:

HotSpot虛擬機的改良   複製算法算法

 

(A)、弱代理論

       分代垃圾收集基於弱代理論(weak generational hypothesis),具體描述如下:

       (1)、大多數分配了內存的對象並不會存活太長時間,在處於年輕代時就會死掉;

       (2)、很少有對象會從老年代變成年輕代;

            其中IBM研究表明:新生代中98%的對象都是"朝生夕死";

            所以並不需要按1:1比例來劃分內存(解決了缺點1);

(B)、HotSpot虛擬機新生代內存佈局及算法

      (1)、將新生代內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間;

      (2)、每次使用Eden和其中一塊Survivor;

      (3)、當回收時,將Eden和使用中的Survivor中還存活的對象一次性複製到另外一塊Survivor;

      (4)、而後清理掉Eden和使用過的Survivor空間;

      (5)、後面就使用Eden和複製到的那一塊Survivor空間,重複步驟3;

默認Eden:Survivor=8:1,即每次可以使用90%的空間,只有一塊Survivor的空間被浪費;

(C)、分配擔保

       如果另一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制(Handle Promotion)進入老年代;

       分配擔保在以後講解垃圾收集器執行規則時再詳解;

 

  • 標記-整理算法

 "標記-整理"(Mark-Compact)算法是根據老年代的特點提出的。

1、算法思路

(1)、標記

      標記過程與"標記-清除"算法一樣;

(2)、整理

       但後續不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動;

       然後直接清理掉端邊界以外的內存;

2、優點

(A)、不會像複製算法,效率隨對象存活率升高而變低

       老年代特點:

       對象存活率高,沒有額外的空間可以分配擔保;

       所以老年代一般不能直接選用複製算法算法;

       而選用標記-整理算法;

(B)、不會像標記-清除算法,產生內存碎片

       因爲清除前,進行了整理,存活對象都集中到空間一側;

3、缺點

       主要是效率問題:除像標記-清除算法的標記過程外,還多了需要整理的過程,效率更低;

4、應用場景

       很多垃圾收集器採用這種算法來回收老年代;

      如Serial Old收集器、G1(從整體看);

標記整理過程如下:

 

  • 分代收集算法

 "分代收集"(Generational Collection)算法結合不同的收集算法處理不同區域。

1、算法思路

       基於前面說的弱代理論,其實並沒有什麼新的思想;

       只是根據對象存活週期的不同將內存劃分爲幾塊;

       這樣就可以根據各個年代的特點採用最適當的收集算法;

       一般把Java堆分爲新生代和老年代;

(A)、新生代

       每次垃圾收集都有大批對象死去,只有少量存活

       所以可採用複製算法;

(B)、老年代

       對象存活率高,沒有額外的空間可以分配擔保;

      使用"標記-清理"或"標記-整理"算法;
 

2、優點      

       可以根據各個年代的特點採用最適當的收集算法;

3、缺點      

       仍然不能控制每次垃圾收集的時間;
4、應用場景

      目前幾乎所有商業虛擬機的垃圾收集器都採用分代收集算法;

      如HotSpot虛擬機中全部垃圾收集器:Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1(也保留);
 

 

二、垃圾收集器有那些

 

JDK7/8後,HotSpot虛擬機所有收集器及組合(連線)

圖展示了七種作用於不同分代的收集器,如果兩個收集器之間存在連線,就說明它們可以搭配使用,圖中收集器所處的區域,則表示它是屬於新生代收集器抑或是老年代收集器。接下來將逐一介紹這些收集器的目標、特性、原理和使用場景,並重點分析CMS和G1這兩款相對複雜而又廣泛使用的收集器,深入瞭解它們的部分運作細節。 

 

(1)、圖中展示了7種不同分代的收集器:

       Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS、G1;

(2)、而它們所處區域,則表明其是屬於新生代收集器還是老年代收集器:

      新生代收集器:Serial、ParNew、Parallel Scavenge;

      老年代收集器:Serial Old、Parallel Old、CMS;

      整堆收集器:G1;

(3)、兩個收集器間有連線,表明它們可以搭配使用:

       [Serial/Serial Old] 、[Serial/CMS] 、[ParNew/Serial Old] 、[ParNew/CMS] 、[Parallel Scavenge/Serial Old]、 [Parallel Scavenge/Parallel Old] 、 [G1] ;

(4)、其中Serial Old作爲CMS出現"Concurrent Mode Failure"失敗的後備預案(後面介紹);
 

併發垃圾收集和並行垃圾收集的區別

(1)、並行(Parallel)

       指多條垃圾收集線程並行工作,但此時用戶線程仍然處於等待狀態

       如ParNew、Parallel Scavenge、Parallel Old;

(2)、併發(Concurrent)

       指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行);

      用戶程序在繼續運行,而垃圾收集程序線程運行於另一個CPU上;    

       如CMS、G1(也有並行);
 

Minor GC和Major GC的區別

(A)、Minor GC

       又稱新生代GC,指發生在新生代的垃圾收集動作;

       因爲Java對象大多是朝生夕滅,所以Minor GC非常頻繁,一般回收速度也比較快;

(B)、Major GC

       又稱Full GC或老年代GC,指發生在老年代的GC;

       出現Major GC經常會伴隨至少一次的Minor GC(不是絕對,Parallel Sacvenge收集器就可以選擇設置Major GC策略);

      Major GC速度一般比Minor GC慢10倍以上
 

 

沒有最好的收集器,更沒有萬能的收集;

選擇的只能是適合具體應用場景的收集器。

 

垃圾回收期的演變歷程

 

1、Serial 收集器

介紹

    Serial收集器是最基礎、歷史最悠久的收集器,曾經(在JDK 1.3.1之前)是HotSpot虛擬機新生代收集器的唯一選擇。

特點

  1. 這個收集器是一個單線程工作的收集器。
  2. 它進行垃圾收集時,必須暫停其他所有工作線程,直到它收集結束。會發生 Stop The World”
  3. 默認新生代收集器。
  4. 採用複製算法;

 Serial/Serial Old組合收集器運行示意圖如下:

應用場景

      依然是HotSpot在Client模式下默認的新生代收集器

      也有優於其他收集器的地方:

      簡單高效(與其他收集器的單線程相比);

      對於限定單個CPU的環境來說,Serial收集器沒有線程交互(切換)開銷,可以獲得最高的單線程收集效率;

      在用戶的桌面應用場景中,可用內存一般不大(幾十M至一兩百M),可以在較短時間內完成垃圾收集(幾十ms至一百多ms),        只要不頻繁發生,這是可以接受的
 

設置參數

      "-XX:+UseSerialGC":添加該參數來顯式的使用串行垃圾收集器;

Stop TheWorld說明:

      JVM在後臺自動發起和自動完成的,在用戶不可見的情況下,把用戶正常的工作線程全部停掉,即GC停頓;

      會帶給用戶不良的體驗;

      從JDK1.3到現在,從Serial收集器--Parallel收集器--CMS--G1,用戶線程停頓時間不斷縮短,但仍然無法完全消除
 

 

2、ParNew收集器

 介紹

    ParNew收集器實質上是 Serial收集器的多線程並行版本

特點:

    除了多線程外,其餘的行爲、特點和Serial收集器一樣;

 如Serial收集器可用的所有控制參數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:                                HandlePromotionFailure   等)、收集算法[複製算法]、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一致

    ParNew 、Serial  這兩種收集器也共用了相當多的代碼。

ParNew/Serial Old組合收集器運行示意圖如下:

應用場景:

      在Server模式下,ParNew收集器是一個非常重要的收集器,因爲除Serial外,目前只有它能與CMS收集器配合工作

      但在單個CPU環境中,不會比Serail收集器有更好的效果,因爲存在線程交互開銷。

設置參數:

      "-XX:+UseConcMarkSweepGC":指定使用CMS後,會默認使用ParNew作爲新生代收集器;

      "-XX:+UseParNewGC":強制指定使用ParNew;    

      "-XX:ParallelGCThreads":指定垃圾收集的線程數量,ParNew默認開啓的收集線程與CPU的數量相同;

爲什麼只有ParNew能與CMS收集器配合:

      CMS是HotSpot在JDK1.5推出的第一款真正意義上的併發(Concurrent)收集器,第一次實現了讓垃圾收集線程與用戶線程(基        本上)同時工作;

      CMS作爲老年代收集器,但卻無法與JDK1.4已經存在的新生代收集器Parallel Scavenge配合工作;

      因爲 Parallel Scavenge(以及G1)都沒有使用傳統的GC收集器代碼框架,而另外獨立實現;而其餘幾種收集器則共用了部分的       框架代碼;

      關於CMS收集器後面會詳細介紹。

 

注意 :  從ParNew收集器開始,後面還將會接觸到若干款涉及“併發”和“並行”概念的收集器。在大家可能產生疑惑之前,有必要先解釋清                 楚這兩個名詞。並行和併發都是併發編程中的專業名詞,在談論垃圾收集器的上下文語境中,它們可以理解爲:

並行(Parallel):並行描述的是多條垃圾收集器線程之間的關係,說明同一時間有多條這樣的線程在協同工作,通常默認此時用戶線程是處於等待狀態。

併發(Concurrent):併發描述的是垃圾收集器線程用戶線程之間的關係,說明同一時間垃圾收集器線程與用戶線程都在運行。由於用戶線程並未被凍結,所以程序仍然能響應服務請求,但由於垃圾收集器線程佔用了一部分系統資源,此時應用程序的處理的吞吐量將受到一定影響

3、Parallel Scavenge收集器

介紹: 

  Parallel Scavenge垃圾收集器因爲與吞吐量關係密切,也稱爲吞吐量收集器(Throughput Collector)

特點:

  有一些特點與ParNew收集器相似

      新生代收集器;

      採用複製算法

      多線程收集;

  主要特點是:它的關注點與其他收集器不同

      CMS等收集器的關注點是儘可能地縮短垃圾收集時用戶線程的停頓時間

      而Parallel Scavenge收集器的目標則是 達到一個可控制的 吞吐量(Throughput)

         【Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是儘可能地 縮短垃圾收集時 用戶             線 程的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是處理器             用於運行用戶代碼的時間與處理器  消耗時間  的比值 】 

      關於吞吐量與收集器關注點說明詳見本節後面;
 

應用場景:

      高吞吐量爲目標,即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間;

      當應用程序運行在具有多個CPU上,對暫停時間沒有特別高的要求時,即程序主要在後臺進行計算,而不需要與用戶進行太多交互;

      例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序;
 

設置參數:

 Parallel Scavenge收集器提供兩個參數用於精確控制吞吐量:

(1)、"-XX:MaxGCPauseMillis"

      控制最大垃圾收集停頓時間,大於0的毫秒數;

      MaxGCPauseMillis設置得稍小,停頓時間可能會縮短,但也可能會使得吞吐量下降;

      因爲可能導致垃圾收集發生得更頻繁;不過大家不要異想天開地認爲如果把這個參數的值設置得更小一點就能使得系統的垃圾收        集速度變得更快,垃圾收集停頓時間縮短是以犧牲吞吐量和新生代空間爲代價換取的:系統把新生代調得小一些,收集300MB新        生代肯定比收集500MB快,但這也直接導致垃圾收集發生得更頻繁,原來10秒收集一次、每次停頓100毫秒,現在變成5秒收集          一次、每次停頓70毫秒。停頓時間的確在下降,但吞吐量也降下來了。

(2)、"-XX:GCTimeRatio"

      設置垃圾收集時間佔總時間的比率,0<n<100的整數;

      GCTimeRatio相當於設置吞吐量大小;

      垃圾收集執行時間佔應用程序執行時間的比例的計算方法是:

      1 / (1 + n)

      例如,選項-XX:GCTimeRatio=19,設置了垃圾收集時間佔總時間的5%--1/(1+19);

      默認值是1%--1/(1+99),即n=99;
 

(3)、"-XX:+UseAdptiveSizePolicy"

      開啓這個參數後,就不用手工指定一些細節參數,如:

      新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉升老年代的對象年齡(-                                              XX:PretenureSizeThreshold)等;  

      JVM會根據當前系統運行情況收集性能監控信息,動態調整這些參數,以提供最合適的停頓時間或最大的吞吐量,這種調節方式        稱爲GC自適應的調節策略(GC Ergonomiscs);    


     這是一種值得推薦的方式

      (1)、只需設置好內存數據大小(如"-Xmx"設置最大堆);

      (2)、然後使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"給JVM設置一個優化目標;

      (3)、那些具體細節參數的調節就由JVM自適應完成;        

      這也是Parallel Scavenge收集器與ParNew收集器一個重要區別; 
 

吞吐量與收集器關注點說明:

1、吞吐量(Throughput)

      CPU用於運行用戶代碼的時間CPU總消耗時間的比值;

      即吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間);    

      高吞吐量即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間;

【 如果虛擬機完成某個任務,用戶代碼加上垃圾收集總共耗費了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。停頓時間越短就越適合需要與用戶交互或需要保證服務響應質量的程序,良好的響應速度能提升用戶體驗;而高吞吐量則可以最高效率地利用處理器資源,儘快完成程序的運算任務,主要適合在後臺運算而不需要太多交互的分析任務。】

2、垃圾收集器期望的目標(關注點)

  (1)、停頓時間    

        停頓時間越短就適合需要與用戶交互的程序;

        良好的響應速度能提升用戶體驗;

  (2)、吞吐量

        高吞吐量則可以高效率地利用CPU時間,儘快完成運算的任務;

        主要適合在後臺計算而不需要太多交互的任務;

  (3)、覆蓋區(Footprint)

        在達到前面兩個目標的情況下,儘量減少堆的內存空間;

        可以獲得更好的空間局部性;
 

 

4、Serial Old收集器

介紹: 

    Serial Old是 Serial收集器 的老年代版本

特點:

      針對老年代

      採用"標記-整理"算法(還有壓縮,Mark-Sweep-Compact);

      單線程收集;

 Serial/Serial Old收集器運行示意圖如下:

應用場景

      主要意義也是供 客戶端模式 下的HotSpot虛擬機使用;

      而在Server模式有兩大用途:

      (A)、在JDK1.5及之前,與Parallel Scavenge收集器搭配使用(JDK1.6有Parallel Old收集器可搭配);

      (B)、作爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用(後面詳解);
 

5、Parallel Old收集器

介紹: 

    Parallel Old垃圾收集器是Parallel Scavenge收集器的老年代版本;

      JDK1.6中才開始提供;

特點:

      針對老年代

      採用"標記-整理"算法;

      多線程收集;

Parallel Scavenge/Parallel Old收集器運行示意圖如下:

應用場景:

      JDK1.6及之後用來代替老年代的Serial Old收集器;

      特別是在Server模式,多CPU的情況下;

      這樣在  注重吞吐量以及CPU資源敏感的場景 ,就有了Parallel Scavenge加Parallel Old收集器的"給力"應用組合;

設置參數:

      "-XX:+UseParallelOldGC":指定使用Parallel Old收集器;
 

Parallel Old 收集器是直到JDK 6時纔開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處於相當尷尬的狀態,原因是如果新生代選擇了ParallelScavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器以外別無選擇,其他表現良好的老年代收集器,如CMS無法與它配合工作。由於老年代Serial Old收集器在服務端應用性能上的“拖累”,使用Parallel Scavenge收集器也未必能在整體上獲得吞吐量最大化的效果。同樣,由於單線程的老年代收集中無法充分利用服務器多處理器的並行處理能力,在老年代內存空間很大而且硬件規格比較高級的運行環境中,這種組合的總吞吐量甚至不一定比  ParNew加CMS  的組合來得優秀。

 

6、CMS收集器

介紹:

    併發 "標記-清除"(Concurrent Mark Sweep,CMS)收集器也稱爲併發低停頓收集器(Concurrent Low Pause Collector)或低延        遲(low-latency)垃圾收集器;

    在前面ParNew收集器曾簡單介紹過其特點;

 

特點:

      針對老年代

      基於"標記-清除"算法(不進行壓縮操作,產生內存碎片);            

      以獲取最短回收停頓時間爲目標

      併發收集、低停頓;

      需要更多的內存(看後面的缺點);

 是HotSpot在JDK1.5推出的第一款真正意義上的併發(Concurrent)收集器

 第一次實現了讓垃圾收集線程與用戶線程(基本上)同時工作;

 

應用場景:

      與用戶交互較多的場景;        

      希望系統停頓時間最短,注重服務的響應速度;

      以給用戶帶來較好的體驗;

      如常見WEB、B/S系統的服務器上的應用

 

設置參數:

      "-XX:+UseConcMarkSweepGC":指定使用CMS收集器;

 

CMS收集器運作過程:

      比前面幾種收集器更復雜,可以分爲4個步驟:

(1)、初始標記(CMS initial mark)       -- 需要 “Stop The World”

      僅標記一下GC Roots能直接關聯到的對象;

      速度很快;

      但需要"Stop The World";

(2)、併發標記(CMS concurrent mark)--耗時長 可併發

     【併發標記階段就是從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以         與垃圾收集線程一起併發運行;】      

      進行GC Roots Tracing 的過程;

      剛纔產生的集合中標記出存活對象;

      用戶應用程序也在運行;

      並不能保證可以標記出所有的存活對象;

(3)、重新標記(CMS remark)           -- 需要 “Stop The World”

      爲了修正併發標記期間因用戶程序繼續運作而導致標記變動的那一部分對象的標記記錄;

      需要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;

      採用多線程並行執行來提升效率;

(4)、併發清除(CMS concurrent sweep) --耗時長  可併發

      回收所有的垃圾對象;

 

初始標記、重新標記這兩個步驟仍然需要“Stop The World”

 整個過程中耗時最長的併發標記併發清除都可以與用戶線程一起工作;

 所以總體上說,CMS收集器的內存回收過程與用戶線程一起併發執行;

 CMS收集器運行示意圖如下:

通過圖可以比較清楚地看到CMS收集器的運作步驟中併發和需要停頓的階段。

CMS收集器3個明顯的缺點:

(1)、對CPU資源非常敏感

      併發收集雖然不會暫停用戶線程,但因爲佔用一部分CPU資源,還是會導致應用程序變慢,總吞吐量降低。

      CMS的默認收集線程數量是=(CPU數量+3)/4;  【此處如何得出佔用不超過25%的處理器運算資源 ?
      如果處理器核心數在4個或以上,併發回收時垃圾收集線程只佔用不超過25%的處理器運算資源,並且會隨着處理器核心數量的          增加而下降。但是當處理器核心數量不足四個時,CMS對用戶程序的影響就可能變得很大。

     如果應用本來的處理器負載就很高,還要分出一半的運算能力去執行收集器線程,就可能導致用戶程序的執行速度忽然大幅降低。

爲了緩解這種情況:

      增量式併發收集器:

        針對這種情況,曾出現了"增量式併發收集器"(Incremental Concurrent Mark Sweep/i-CMS);

        類似使用搶佔式來模擬多任務機制的思想,讓收集線程和用戶線程交替運行,減少收集線程運行時間;

        但效果並不理想,JDK1.6後就官方不再提倡用戶使用。
 

(2)、無法處理浮動垃圾,可能出現"Concurrent Mode Failure"失敗

   

      浮動垃圾(Floating Garbage):

        在併發清除時,用戶線程新產生的垃圾,稱爲浮動垃圾;

        這使得併發清除時需要預留一定的內存空間,不能像其他收集器在老年代幾乎填滿再進行收集;

        也要可以認爲CMS所需要的空間比其他垃圾收集器大;

        "-XX:CMSInitiatingOccupancyFraction":設置CMS預留內存空間;

        JDK1.5默認值爲68%;

        JDK1.6變爲大約92%;             

     "Concurrent Mode Failure"失敗:

        如果CMS預留內存空間無法滿足程序需要,就會出現一次"Concurrent Mode Failure"失敗;

        這時JVM啓用後備預案:臨時啓用Serail Old收集器,而導致另一次Full GC的產生;

        這樣的代價是很大的,所以CMSInitiatingOccupancyFraction不能設置得太大。
 

(3)、產生大量內存碎片

      由於CMS基於"標記-清除"算法,清除後不進行壓縮操作;

      前面   "標記-清除"算法  介紹時曾說過:

      產生大量不連續的內存碎片會導致分配大內存對象時,無法找到足夠的連續內存,從而需要提前觸發另一次Full GC動作。
 

     解決方法:                

        "-XX:+UseCMSCompactAtFullCollection"

            使得CMS出現上面這種情況時不進行Full GC,而開啓內存碎片的合併整理過程;

            但合併整理過程無法併發,停頓時間會變長;

            默認開啓(但不會進行,結合下面的CMSFullGCsBeforeCompaction);

       "-XX:+CMSFullGCsBeforeCompaction"

           設置執行多少次不壓縮的Full GC後,來一次壓縮整理;

           爲減少合併整理過程的停頓時間;

           默認爲0,也就是說每次都執行Full GC,不會進行壓縮整理;
  

由於空間不再連續,CMS需要使用可用"空閒列表"內存分配方式,這比簡單實用"碰撞指針"分配內存消耗大;

 總體來看,與Parallel Old垃圾收集器相比,CMS減少了執行老年代垃圾收集時應用暫停的時間;

 但卻增加了新生代垃圾收集時應用暫停的時間、降低了吞吐量而且需要佔用更大的堆空間;

 

7、G1收集器

JDK 9發佈之日,G1宣告取代Parallel Scavenge加Parallel Old組合,成爲服務端模式下的默認垃圾收集器,而CMS則淪落至被聲明爲不推薦使用(Deprecate)的收集器。

G1開創的基於Region的堆內存佈局是它能夠實現這個目標的關鍵。雖然G1也仍是遵循分代收集理論設計的,但其堆內存的佈局與其他收集器有非常明顯的差異:G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分爲多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新創建的對象還是已經存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果。

Region中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認爲只要大小超過了一個Region容量一半的對象即可判定爲大對象。每個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值範圍爲1MB~32MB,且應爲2的N次冪。而對於那些超過了整個Region容量的超級大對象,將會被存放在N個連續的HumongousRegion之中,G1的大多數行爲都把Humongous Region作爲老年代的一部分來進行看待

介紹:

 G1(Garbage-First)是JDK7-u4才推出商用的收集器;

特點:

(1)、並行與併發

      能充分利用多CPU、多核環境下的硬件優勢;

      可以並行來縮短"Stop The World"停頓時間;

      也可以併發讓垃圾收集與用戶程序同時進行;

(2)、分代收集,收集範圍包括新生代和老年代    

      能獨立管理整個GC堆(新生代和老年代),而不需要與其他收集器搭配;

      能夠採用不同方式處理不同時期的對象;

                

      雖然保留分代概念,但Java堆的內存佈局有很大差別;

      將整個堆劃分爲多個大小相等的獨立區域(Region);

      新生代和老年代不再是物理隔離,它們都是一部分Region(不需要連續)的集合;
 

(3)、結合多種垃圾收集算法,空間整合,不產生碎片

      從整體看,是基於  標記-整理  算法;

      從局部(兩個Region間)看,是基於  複製算法

      這是一種類似火車算法的實現;

 

      都不會產生內存碎片,有利於長時間運行;

(4)、可預測的停頓:低停頓的同時實現高吞吐量

      G1除了追求低停頓處,還能建立可預測的停頓時間模型;

      可以明確指定M毫秒時間片內,垃圾收集消耗的時間不超過N毫秒;
 

 

應用場景:

      面向服務端應用,針對具有大內存、多處理器的機器;

      最主要的應用是爲需要低GC延遲,並具有大堆的應用程序提供解決方案;

      如:在堆大小約6GB或更大時,可預測的暫停時間可以低於0.5秒;

            

      用來替換掉JDK1.5中的CMS收集器;

      在下面的情況時,使用G1可能比CMS好:

      (1)、超過50%的Java堆被活動數據佔用;

      (2)、對象分配頻率或年代提升頻率變化很大;

      (3)、GC停頓時間過長(長於0.5至1秒)。
 

設置參數

      "-XX:+UseG1GC":指定使用G1收集器;

      "-XX:InitiatingHeapOccupancyPercent":當整個Java堆的佔用率達到參數值時,開始併發標記階段;默認爲45;

      "-XX:MaxGCPauseMillis":爲G1設置暫停時間目標,默認值爲200毫秒;

      "-XX:G1HeapRegionSize":設置每個Region大小,範圍1MB到32MB;目標是在最小Java堆時可以擁有約2048個Region;
 

爲什麼G1收集器可以實現可預測的停頓

      G1可以建立可預測的停頓時間模型,是因爲:

      可以有計劃地避免在Java堆的進行全區域的垃圾收集;

      G1跟蹤各個Region獲得其收集價值大小,在後臺維護一個優先列表;

      每次根據允許的收集時間,優先回收價值最大的Region(名稱Garbage-First的由來);

      這就保證了在有限的時間內可以獲取儘可能高的收集效率;
 

一個對象被不同區域引用的問題

      一個Region不可能是孤立的,一個Region中的對象可能被其他任意Region中對象引用,判斷對象存活時,是否需要掃描整個              Java堆才能保證準確?

      在其他的分代收集器,也存在這樣的問題(而G1更突出):

      回收新生代也不得不同時掃描老年代?

      這樣的話會降低Minor GC的效率;

      解決方法:

           無論G1還是其他分代收集器,JVM都是使用Remembered Set來避免全局掃描

           每個Region都有一個對應的Remembered Set;

           每次Reference類型數據寫操作時,都會產生一個Write Barrier暫時中斷操作;

           然後檢查將要寫入的引用指向的對象是否和該Reference類型數據在不同的Region(其他收集器:檢查老年代對象是否引用了新生代對象);

           如果不同,通過CardTable把相關引用信息記錄到引用指向對象的所在Region對應的Remembered Set中;

           當進行垃圾收集時,在GC根節點的枚舉範圍加入Remembered Set;

           就可以保證不進行全局掃描,也不會有遺漏。

 

G1收集器運作過程

不計算維護Remembered Set的操作,可以分爲4個步驟(與CMS較爲相似)。

(1)、初始標記(Initial Marking)

      僅標記一下GC Roots能直接關聯到的對象;

      且修改TAMS(Next Top at Mark Start),讓下一階段併發運行時,用戶程序能在正確可用的Region中創建新對象;

      需要"Stop The World",但速度很快;

(2)、併發標記(Concurrent Marking)

      進行GC Roots Tracing的過程;

      剛纔產生的集合中標記出存活對象;

      耗時較長,但應用程序也在運行;

      並不能保證可以標記出所有的存活對象;

(3)、最終標記(Final Marking)

      爲了修正併發標記期間因用戶程序繼續運作而導致標記變動的那一部分對象的標記記錄;

      上一階段對象的變化記錄在線程的Remembered Set Log;

      這裏把Remembered Set Log合併到Remembered Set中;

                    

      需要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;

      採用多線程並行執行來提升效率;

(4)、篩選回收(Live Data Counting and Evacuation)

      首先排序各個Region的回收價值和成本;

      然後根據用戶期望的GC停頓時間來制定回收計劃;

      最後按計劃回收一些價值高的Region中垃圾對象;

      可以自由選擇任意多個Region構成回收集,然後把決定回收的那一部分Region的存活對象複製到空的Region中,再清理掉整個舊Region的全部空間。這裏的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程並行完成的。

                    

      回收時採用"複製"算法,從一個或多個Region複製存活對象到堆上的另一個空的Region,並且在此過程中壓縮和釋放內存;

      可以併發進行,降低停頓時間,並增加吞吐量;
 

  G1收集器運行示意圖如下:

其實本也有想過設計成與用戶程序一起併發執行,但這件事情做起來比較複雜,考慮到G1只是回收一部分Region,停頓時間是用戶可控制的,所以並不迫切去實現,而選擇把這個特性放到了G1之後出現的低延遲垃圾收集器(即ZGC)中。另外,還考慮到G1不是僅僅面向低延遲,停頓用戶線程能夠最大幅度提高垃圾收集效率,爲了保證吞吐量所以才選擇了完全暫停用戶線程的實現方案。通過圖可以比較清楚地看到G1收集器的運作步驟中併發和需要停頓的階段。

G1收集器除了併發標記外,其餘階段也是要完全暫停用戶線程的,換言之,它並非純粹地追求低延遲,官方給它設定的目標是在延遲可控的情況下獲得儘可能高的吞吐量,所以才能擔當起“全功能收集器”的重任與期望。

 

G1內存佈局:

 

雖然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它們都是一系列區域(不需要連續)的動態集合。

 

圖中: ERegion扮演新生代的Eden空間;    S : Region扮演Survivor空間 ;     H:Humongous區域作爲老年代的一部分

 

G1開創的基於Region的堆內存佈局,上圖就是Region的堆內存佈局,雖然G1仍是遵循分代收集理論設計的,但其堆內存的佈局與其他收集器有非常明顯的差異。G1不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分爲多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間Survivor空間,或者老年代空間

Humongous區域,專門用來存儲大對象 ,G1的大多數行爲都把Humongous Region作爲老年代的一部分來進行看待。

 

  • 獨立區域(Region)
  • 每個Region賦予角色(新生代的Eden空間、Survivor空間、老年代空間)
  • 每個角色的Region採用不同的垃圾回收算法
  • TAMS(Top at Mark Strar)指針
  • 每個Region單獨劃分出的一塊空間
  • 用於在併發回收過程中新對象的分配
  • Humongous Regoin
  • 專門存儲大對象(G1認爲只要大小超過了一個Region容量一半的對象即可判定爲大對象

 

G1收集器關於停頓時間:

1、可以由用戶指定期望的停頓時間是G1收集器很強大的一個功能,設置不同的期望停頓時間。

2、可使得G1在不同應用場景中取得關注吞吐量和關注延遲之間的最佳平衡。

3、設置一個合理的停頓時長:

不過,這裏設置的“期望值”必須是符合實際的,不能異想天開,畢竟G1是要凍結用戶線程來複制對象的,這個停頓時間再怎麼低也得有個限度。它 默認的停頓 目標爲200毫秒,一般來說,回收階段佔到幾十到一百甚至接近兩百毫秒都很正常,但如果我們把停頓時間調得非常低,譬如設置爲二十毫秒,很可能出現的結果就是由於停頓目標時間太短,導致每次選出來的回收集只佔堆內存很小的一部分,收集器收集的速度逐漸跟不上分配器分配的速度,導致垃圾慢慢堆積。很可能一開始收集器還能從空閒的堆內存中獲得一些喘息的時間,但應用運行時間一長就不行了,最終佔滿堆引發Full GC反而降低性能,所以通常把期望停頓時間設置爲100-200毫秒或者200-300會是比較合理的。

 

G1收集器是垃圾收集技術發展的里程碑

開創了收集器 面向局部收集的設計思路 和 基於Region的內存佈局形式。從G1開始,最先進的垃圾收集器的設計導向都不約而同地變爲 追求能夠應付應用的內存分配速率(Allocation Rate),而不追求一次把整個Java堆全部清理乾淨。這樣,應用在分配,同時收集器在收集,只要收集的速度能跟得上對象分配的速度,那一切就能運作得很完美。這種新的收集器設計思路從工程實現上看是從G1開始興起的,所以說G1是收集器技術發展的一個里程碑。

 

G1收集器 和 CMS收集器對比:

1、G1可以指定最大停頓時間

2、G1分Region的內存佈局,CMS整塊內存

3、G1收集器的Mixed GC模式,與其他收集器的不同。

在G1收集器出現之前的所有其他收集器,包括CMS在內,垃圾收集的目標範圍要麼是整個新生代(MinorGC),要麼就是整個老年代(Major GC),再要麼就是整個Java堆(Full GC)。而G1跳出了這個樊籠,它可以面向堆內存任何部分來組成回收集(Collection Set,一般簡稱CSet)進行回收,衡量標準不再是它屬於哪個分代,而是哪塊內存中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式。

4、收集算法角度上:

G1從整體來看是基於“標記-整理”算法實現的收集器,但從局部(兩個Region之間)上看又是基於“標記-複製”算法實現;CMS使用“標記-清除”算法實現。

5、內存角度上:

在用戶程序運行過程中,G1無論是爲了垃圾收集產生的內存佔用(Footprint)還是程序運行時的額外執行負載(Overload)都要比CMS要高。 

   介紹:  關於G1和CMS內存佔用的區別   :   雖然G1和CMS都使用卡表來處理跨代指針,但G1的卡表實現更爲複雜,而且堆中每個Region,無論扮演的是新生代還是老年代角色,都必須有一份卡表,這導致G1的記憶集(和其他內存消耗)可能會佔整個堆容量的20%乃至更多的內存空間;相比起來CMS的卡表就相當簡單,只有唯一一份,而且只需要處理老年代到新生代的引用,反過來則不

需要,由於新生代的對象具有朝生夕滅的不穩定性,引用變化頻繁,能省下這個區域的維護開銷是很划算的

6、執行負載的角度上

它們都使用到寫屏障,CMS用寫後屏障來更新維護卡表;而G1除了使用寫後屏障來進行同樣的(由於G1的卡表結構複雜,其實是更煩瑣的)卡表維護操作外,爲了實現原始快照搜索(SATB)算法,還需要使用寫前屏障來跟蹤併發時的指針變化情況。

 

結論:

針對於最終哪款收集器要更好、要好上多少,往往是針對具體場景才能做的定量比較。目前在小內存應用上CMS的表現大概率仍然要會優於G1,而在大內存應用上G1則大多能發揮其優勢,這個優劣勢的Java堆容量平衡點通常在6GB至8GB之間,

 

 

 

 

 

 

 

 

 

 

 

 

 

補充:

只要運行java-version,便可以知道該JVM是運行在Client模式還是Server模式。

如下:

首先,我們來看下:

(1)32位 jdk 1.8中,默認的java -version的輸出結果:

可以看到有Client VM,即運行的是客戶端模式。

(2)64位 jdk 1.8中,默認的java -version的輸出結果:


可以看到有Server VM,即運行的是服務端模式。

補充知識點:

如果是64位的jdk 1.8 amd64,只能運行在Server模式下。而32位的jdk 1.8 i386,默認是運行在client模式下,可以通過修改jdk/jre/lib/i386/jvm.cfg文件裏面的設置來指定默認的啓動模式。默認32位jdk 1.8 i386爲:

-client IF_SERVER_CLASS -server
-server KNOWN
-minimal KNOWN


將第一行註釋,之後有:

#-client IF_SERVER_CLASS -server
-server KNOWN
-minimal KNOWN
 

 

 

 

參考文獻
《深入理解Java虛擬機:JVM高級特性與最佳實踐》
《Java虛擬機規範 Java SE 8版》
《Java併發編程的藝術》
《How to Handle Java Finalization's Memory-Retention Issues》
《Effective Java》第二版 第2章 第7條:避免使用終結方法;
《Thinking in Java》第四版 5.5 清理:終結處理和垃圾回收;
《Java語言規範》12.6 類實例的終結;
 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章