young | Tenured | JVM options |
---|---|---|
Serial | Serial | -XX:+UseSerialGC |
Parallel Scavenge | Serial | -XX:+UseParallelGC -XX:-UseSerialOldGC |
Parallel Scavenge | Parallel Old | -XX:+UseParallelGC -XX:+UseParallelOldGC |
Parallel New或Serial | CMS | -XX:+UseParNewGC -XX:+UseConcMarkSweepGC |
G1 | -XX:+UseG1GC |
垃圾回收器從線程運行狀況分類有三種linux
cms是最經常使用的垃圾垃圾回收器,下面分析下CMS垃圾回收器工做原理;ios
CMS 處理過程有七個步驟:
1. 初始標記(CMS-initial-mark) ,會致使swt;算法
2. 併發標記(CMS-concurrent-mark),與用戶線程同時運行;
3. 預清理(CMS-concurrent-preclean),與用戶線程同時運行;
bash
4. 可被終止的預清理(CMS-concurrent-abortable-preclean) 與用戶線程同時運行;
5. 從新標記(CMS-remark) ,會致使swt;
markdown
6. 併發清除(CMS-concurrent-sweep),與用戶線程同時運行;
7. 併發重置狀態等待下次CMS的觸發(CMS-concurrent-reset),與用戶線程同時運行;
網絡
cms運行流程圖以下所示:
數據結構
下面抓取一下gc信息,來進行詳細分析,首先將jvm中加入如下運行參數:
多線程
-XX:+PrintCommandLineFlags [0]
-XX:+UseConcMarkSweepGC [1]
-XX:+UseCMSInitiatingOccupancyOnly [2]
-XX:CMSInitiatingOccupancyFraction=80 [3]
-XX:+CMSClassUnloadingEnabled [4]
-XX:+UseParNewGC [5]
-XX:+CMSParallelRemarkEnabled [6]
-XX:+CMSScavengeBeforeRemark [7]
-XX:+UseCMSCompactAtFullCollection [8]
-XX:CMSFullGCsBeforeCompaction=0 [9]
-XX:+CMSConcurrentMTEnabled [10]
-XX:ConcGCThreads=4 [11]
-XX:+ExplicitGCInvokesConcurrent [12]
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses [13]
-XX:+CMSParallelInitialMarkEnabled [14]
-XX:+PrintGCDetails [15]
-XX:+PrintGCCause [16]
-XX:+PrintGCTimeStamps [17]
-XX:+PrintGCDateStamps [18]
-Xloggc:../logs/gc.log [19]
-XX:+HeapDumpOnOutOfMemoryError [20]
-XX:HeapDumpPath=../dump [21]
複製代碼
先來介紹下下面幾個參數的做用: 併發
0. [0]打印出啓動參數行3. [4]開啓永久帶(jdk1.8如下版本)或元數據區(jdk1.8及其以上版本)收集,若是沒有設置這個標誌,一旦永久代或元數據區耗盡空間也會嘗試進行垃圾回收,可是收集不會是並行的,而再一次進行Full GC; 4. [5] 使用cms時默認這個參數就是打開的,不須要配置,cms只回收老年代,年輕帶只能配合Parallel New或Serial回收器; app
5. [6] 減小Remark階段暫停的時間,啓用並行Remark,若是Remark階段暫停時間長,能夠啓用這個參數
6. [7] 若是Remark階段暫停時間太長,能夠啓用這個參數,在Remark執行以前,先作一次ygc。由於這個階段,年輕帶也是cms的gcroot,cms會掃描年輕帶指向老年代對象的引用,若是年輕帶有大量引用須要被掃描,會讓Remark階段耗時增長;
7. [8]、[9]兩個參數是針對cms垃圾回收器碎片作優化的,CMS是不會移動內存的, 運行時間長了,會產生不少內存碎片, 致使沒有一段連續區域能夠存放大對象,出現」promotion failed」、」concurrent mode failure」, 致使fullgc,啓用UseCMSCompactAtFullCollection 在FULL GC的時候, 對年老代的內存進行壓縮。-XX:CMSFullGCsBeforeCompaction=0 則是表明多少次FGC後對老年代作壓縮操做,默認值爲0,表明每次都壓縮, 把對象移動到內存的最左邊,可能會影響性能,可是能夠消除碎片; 106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs]
(concurrent mode failure): 41568K->27787K(49152K), 0.2128504 secs] 52402K->27787K(63936K), [CMS Perm : 2086K->2086K(12288K)], 0.2499776 secs] [Times: user=0.28 sys=0.00, real=0.25 secs]
8. [11]定義併發CMS過程運行時的線程數。好比value=4意味着CMS週期的全部階段都以4個線程來執行。儘管更多的線程會加快併發CMS過程,但其也會帶來額外的同步開銷。所以,對於特定的應用程序,應該經過測試來判斷增長CMS線程數是否真的可以帶來性能的提高。若是未設置這個參數,JVM會根據並行收集器中的-XX:ParallelGCThreads參數的值來計算出默認的並行CMS線程數: ParallelGCThreads = (ncpus <=8 ? ncpus : 8+(ncpus-8)*5/8) ,ncpus爲cpu個數,
ConcGCThreads =(ParallelGCThreads + 3)/4這個參數通常不要本身設置,使用默認就好,除非發現默認的參數有調整的必要;
9. [12]、[13]開啓foreground CMS GC,CMS gc 有兩種模式,background和foreground,正常的cms gc使用background模式,就是咱們平時說的cms gc;當併發收集失敗或者調用了System.gc()的時候,就會致使一次full gc,這個fullgc是否是cms回收,而是Serial單線程回收器,加入了參數[12]後,執行full gc的時候,就變成了CMS foreground gc,它是並行full gc,只會執行cms中stop the world階段的操做,效率比單線程Serial full GC要高;須要注意的是它只會回收old,由於cms收集器是老年代收集器;而正常的Serial收集是包含整個堆的,加入了參數[13],表明永久帶也會被cms收集; 10. [14] 開啓初始標記過程當中的並行化,進一步提高初始化標記效率;
11. [15]、[16]、[17]、[18] 、[19]是打印gc日誌,其中[16]在jdk1.8以後無需設置下面就是該參數設置打印出來的gc信息,一些非關鍵的信息已經去掉,如時間:
//第一步 初始標記 這一步會停頓 [GC (CMS Initial Mark) [1 CMS-initial-mark: 299570K(307200K)] 323315K(491520K), 0.0026208 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count 0.345: CMS_Initial_Mark [ 10 0 1 ] [ 0 0 0 0 2 ] 0 Total time for which application threads were stopped: 0.0028494 seconds //第二步 併發標記 [CMS-concurrent-mark-start] [CMS-concurrent-mark: 0.012/0.012 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] //第三步 預清理 [CMS-concurrent-preclean-start] [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //第四步 可被終止的預清理 [CMS-concurrent-abortable-preclean-start] [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] //第五步 從新標記 [GC (CMS Final Remark) [YG occupancy: 72704 K (184320 K)][Rescan (parallel) , 0.0009069 secs][weak refs processing, 0.0000083 secs][class unloading, 0.0002626 secs][scrub symbol table, 0.0003789 secs][scrub string table, 0.0001326 secs][1 CMS-remark: 299570K(307200K)] 372275K(491520K), 0.0017842 secs] [Times: user=0.05 sys=0.00, real=0.00 secs] vmop [threads: total initially_running wait_to_block] [time: spin block sync cleanup vmop] page_trap_count 0.360: CMS_Final_Remark [ 10 0 1 ] [ 0 0 0 0 1 ] 0 Total time for which application threads were stopped: 0.0018800 seconds //第六步 清理 [CMS-concurrent-sweep-start] [CMS-concurrent-sweep: 0.007/0.007 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] //第七步 重置 [CMS-concurrent-reset-start] [CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 複製代碼
這是CMS中兩次stop-the-world事件中的一次。這一步的做用是標記存活的對象,有兩部分:
1. 標記老年代中全部的GC Roots對象,以下圖節點1;
2. 標記年輕代中活着的對象引用到的老年代的對象(指的是年輕代中還存活的引用類型對象,引用指向老年代中的對象)以下圖節點二、3;
在Java語言裏,可做爲GC Roots對象的包括以下幾種:
1. 虛擬機棧(棧楨中的本地變量表)中的引用的對象 ;
2. 方法區中的類靜態屬性引用的對象 ;
3. 方法區中的常量引用的對象 ;
4. 本地方法棧中JNI的引用的對象;
ps:爲了加快此階段處理速度,減小停頓時間,能夠開啓初始標記並行化,-XX:+CMSParallelInitialMarkEnabled,同時調大並行標記的線程數,線程數不要超過cpu的核數;
從「初始標記」階段標記的對象開始找出全部存活的對象;
由於是併發運行的,在運行期間會發生新生代的對象晉升到老年代、或者是直接在老年代分配對象、或者更新老年代對象的引用關係等等,對於這些對象,都是須要進行從新標記的,不然有些對象就會被遺漏,發生漏標的狀況。爲了提升從新標記的效率,該階段會把上述對象所在的Card標識爲Dirty,後續只需掃描這些Dirty Card的對象,避免掃描整個老年代; 併發標記階段只負責將引用發生改變的Card標記爲Dirty狀態,不負責處理;
以下圖所示,也就是節點一、二、3,最終找到了節點4和5。併發標記的特色是和應用程序線程同時運行。並非老年代的全部存活對象都會被標記,由於標記的同時應用程序會改變一些對象的引用等。
這個階段由於是併發的容易致使concurrent mode failure
前一個階段已經說明,不能標記出老年代所有的存活對象,是由於標記的同時應用程序會改變一些對象引用,這個階段就是用來處理前一個階段由於引用關係改變致使沒有標記到的存活對象的,它會掃描全部標記爲Direty的Card 以下圖所示,在併發清理階段,節點3的引用指向了6;則會把節點3的card標記爲Dirty;
最後將6標記爲存活,以下圖所示:
這個階段嘗試着去承擔下一個階段Final Remark階段足夠多的工做。這個階段持續的時間依賴好多的因素,因爲這個階段是重複的作相同的事情直到發生aboart的條件(好比:重複的次數、多少許的工做、持續的時間等等)之一纔會中止。
ps:此階段最大持續時間爲5秒,之因此能夠持續5秒,另一個緣由也是爲了期待這5秒內可以發生一次ygc,清理年輕帶的引用,是的下個階段的從新標記階段,掃描年輕帶指向老年代的引用的時間減小;
這個階段會致使第二次stop the word,該階段的任務是完成標記整個年老代的全部的存活對象。 這個階段,從新標記的內存範圍是整個堆,包含_young_gen和_old_gen。爲何要掃描新生代呢,由於對於老年代中的對象,若是被新生代中的對象引用,那麼就會被視爲存活對象,即便新生代的對象已經不可達了,也會使用這些不可達的對象當作cms的「gc root」, 來掃描老年代; 所以對於老年代來講,引用了老年代中對象的新生代的對象,也會被老年代視做「GC ROOTS」:當此階段耗時較長的時候,能夠加入參數-XX:+CMSScavengeBeforeRemark,在從新標記以前,先執行一次ygc,回收掉年輕帶的對象無用的對象,並將對象放入倖存帶或晉升到老年代,這樣再進行年輕帶掃描時,只須要掃描倖存區的對象便可,通常倖存帶很是小,這大大減小了掃描時間 因爲以前的預處理階段是與用戶線程併發執行的,這時候可能年輕帶的對象對老年代的引用已經發生了不少改變,這個時候,remark階段要花不少時間處理這些改變,會致使很長stop the word,因此一般CMS儘可能運行Final Remark階段在年輕代是足夠乾淨的時候。
另外,還能夠開啓並行收集:-XX:+CMSParallelRemarkEnabled
經過以上5個階段的標記,老年代全部存活的對象已經被標記而且如今要經過Garbage Collector採用清掃的方式回收那些不能用的對象了。
這個階段主要是清除那些沒有標記的對象而且回收空間;
因爲CMS併發清理階段用戶線程還在運行着,伴隨程序運行天然就還會有新的垃圾不斷產生,這一部分垃圾出如今標記過程以後,CMS沒法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲「浮動垃圾」。
這個階段併發執行,從新設置CMS算法內部的數據結構,準備下一個CMS生命週期的使用。
CMS不是full GC
有一點須要注意的是:CMS併發GC不是「full GC」。HotSpot VM裏對concurrent collection和full collection有明確的區分。全部帶有「FullCollection」字樣的VM參數都是跟真正的full GC相關,而跟CMS併發GC無關的,cms收集算法只是清理老年代。
通常CMS的GC耗時 80%都在remark階段,若是發現remark階段停頓時間很長,能夠嘗試添加該參數:
-XX:+CMSScavengeBeforeRemark
複製代碼
在執行remark操做以前先作一次ygc,目的在於減小ygen對oldgen的無效引用,下降remark時的開銷,若是添加該參數後 」ygc停頓時間+remark時間<添加該參數以前的remark時間「,說明該參數是有效的;
CMS是基於標記-清除算法的,只會將標記爲爲存活的對象刪除,並不會移動對象整理內存空間,會形成內存碎片,這時候咱們須要用到這個參數;
-XX:CMSFullGCsBeforeCompaction=n
複製代碼
個參數大部分人的使用方式都是錯誤的,每每會致使設置後問題更大。
CMSFullGCsBeforeCompaction這個參數在HotSpot VM裏是這樣聲明的:
product(bool, UseCMSCompactAtFullCollection, true, \ "Use mark sweep compact at full collections") \ \ product(uintx, CMSFullGCsBeforeCompaction, 0, \ "Number of CMS full collection done before compaction if > 0") \ 複製代碼
而後這樣使用的:
*should_compact = UseCMSCompactAtFullCollection && ((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) || GCCause::is_user_requested_gc(gch->gc_cause()) || gch->incremental_collection_will_fail(true /* consult_young */)); 複製代碼
CMS GC要決定是否在full GC時作壓縮,會依賴幾個條件。其中,
1. UseCMSCompactAtFullCollection 與 CMSFullGCsBeforeCompaction 是搭配使用的;前者目前默認就是true了,也就是關鍵在後者上。
2. 用戶調用了System.gc(),並且DisableExplicitGC沒有開啓。
3. young gen報告接下來若是作增量收集會失敗;簡單來講也就是young gen預計old gen沒有足夠空間來容納下次young GC晉升的對象。
上述三種條件的任意一種成立都會讓CMS決定此次作full GC時要作壓縮。
CMSFullGCsBeforeCompaction 說的是,在上一次CMS併發GC執行事後,到底還要再執行多少次full GC纔會作壓縮。默認是0,也就是在默認配置下每次CMS GC頂不住了而要轉入full GC的時候都會作壓縮。 若是把CMSFullGCsBeforeCompaction配置爲10,就會讓上面說的第一個條件變成每隔10次真正的full GC才作一次壓縮(而不是每10次CMS併發GC就作一次壓縮,目前VM裏沒有這樣的參數)。這會使full GC更少作壓縮,也就更容易使CMS的old gen受碎片化問題的困擾。 原本這個參數就是用來配置下降full GC壓縮的頻率,以期減小某些full GC的暫停時間。CMS回退到full GC時用的算法是mark-sweep-compact,但compaction是可選的,不作的話碎片化會嚴重些但此次full GC的暫停時間會短些;這是個取捨。
這個異常發生在cms正在回收的時候。執行CMS GC的過程當中,同時業務線程也在運行,當年輕帶空間滿了,執行ygc時,須要將存活的對象放入到老年代,而此時老年代空間不足,這時CMS尚未機會回收老年帶產生的,或者在作Minor GC的時候,新生代救助空間放不下,須要放入老年代,而老年代也放不下而產生的。 設置cms觸發時機有兩個參數:
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
複製代碼
-XX:CMSInitiatingOccupancyFraction=70 是指設定CMS在對內存佔用率達到70%的時候開始GC。
-XX:+UseCMSInitiatingOccupancyOnly若是不指定, 只是用設定的回收閾值CMSInitiatingOccupancyFraction,則JVM僅在第一次使用設定值,後續則自動調整會致使上面的那個參數不起做用。
爲何要有這兩個參數?
因爲在垃圾收集階段用戶線程還須要運行,那也就還須要預留有足夠的內存空間給用戶線程使用,所以CMS收集器不能像其餘收集器那樣等到老年代幾乎徹底被填滿了再進行收集,須要預留一部分空間提供併發收集時的程序運做使用。
CMS前五個階段都是標記存活對象的,除了」初始標記」和」從新標記」階段會stop the word ,其它三個階段都是與用戶線程一塊兒跑的,就會出現這樣的狀況gc線程正在標記存活對象,用戶線程同時向老年代提高新的對象,清理工做尚未開始,old gen已經沒有空間容納更多對象了,這時候就會致使concurrent mode failure, 而後就會使用串行收集器回收老年代的垃圾,致使停頓的時間很是長。
CMSInitiatingOccupancyFraction參數要設置一個合理的值,設置大了,會增長concurrent mode failure發生的頻率,設置的小了,又會增長CMS頻率,因此要根據應用的運行狀況來選取一個合理的值。
若是發現這兩個參數設置大了會致使fullgc,設置小了會致使頻繁的cmsgc,說明你的老年代空間太小,應該增長老年代空間的大小了;
這個異常發生在年輕帶回收的時候;
在進行Minor GC時,Survivor Space放不下,對象只能放入老年代,而此時老年代也放不下形成的,多數是因爲老年帶有足夠的空閒空間,可是因爲碎片較多,新生代要轉移到老年帶的對象比較大,找不到一段連續區域存放這個對象致使的,如下是一段promotion failed的日誌:
106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs] (concurrent mode failure): 41568K->27787K(49152K), 0.2128504 secs] 52402K->27787K(63936K), [CMS Perm : 2086K->2086K(12288K)], 0.2499776 secs] [Times: user=0.28 sys=0.00, real=0.25 secs]
在 Minor GC 過程當中,Survivor Unused 可能不足以容納 Eden 和另外一個 Survivor 中的存活對象, 那麼多餘的將被移到老年代, 稱爲過早提高(Premature Promotion),這會致使老年代中短時間存活對象的增加,可能會引起嚴重的性能問題。 再進一步, 若是老年代滿了, Minor GC 後會進行 Full GC, 這將致使遍歷整個堆, 稱爲提高失敗(Promotion Failure)。
1. Survivor空間過小,容納不下所有的運行時短生命週期的對象,若是是這個緣由,能夠嘗試將Survivor調大,不然端生命週期的對象提高過快,致使老年代很快就被佔滿,從而引發頻繁的full gc;
2. 對象太大,Survivor和Eden沒有足夠大的空間來存放這些大象;
當提高的時候,發現老年代也沒有足夠的連續空間來容納該對象。
爲何是沒有足夠的連續空間而不是空閒空間呢?
老年代容納不下提高的對象有兩種狀況:
1. 老年代空閒空間不夠用了;
2. 老年代雖然空閒空間不少,可是碎片太多,沒有連續的空閒空間存放該對象;
1. 若是是由於內存碎片致使的大對象提高失敗,cms須要進行空間整理壓縮;
2. 若是是由於提高過快致使的,說明Survivor 空閒空間不足,那麼能夠嘗試調大 Survivor;
3. 若是是由於老年代空間不夠致使的,嘗試將CMS觸發的閾值調低;
[Times: user=0.00 sys=0.00, real=0.00 secs] user是用戶線程佔用的時間,sys是系統線程佔用的時間,若是是io致使的問題,會有兩種狀況
1. user與sys時間都很是小,可是real卻很長,以下:
[ Times: user=0.51 sys=0.10, real=5.00 secs ]
複製代碼
user+sys的時間遠遠小於real的值,這種狀況說明停頓的時間並非消耗在cup執行上了,不是cup確定就是io致使的了,因此這時候要去檢查系統的io狀況。
[ Times: user=0.11 sys=31.10, real=33.12 secs ]
複製代碼
這時候其中一種緣由是開啓了大內存頁,還開啓了swap,大內存進行swap交換時會有這種現象;
CMS默認啓動的回收線程數目是 (ParallelGCThreads + 3)/4) ,這裏的ParallelGCThreads是年輕代的並行收集線程數,感受有點怪怪的;
年輕代的並行收集線程數默認是(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8),能夠經過-XX:ParallelGCThreads= N 來調整; 若是要直接設定CMS回收線程數,能夠經過-XX:ParallelCMSThreads=n,注意這個n不能超過cpu線程數,須要注意的是增長gc線程數,就會和應用爭搶資源;