本篇文章主要在狸貓技術窩中有關JVM中調優的一些實戰基礎上進行總結,能夠算是本身的一篇學習總結。主要以目前主流的兩種垃圾回收組合方式,ParNew +CMS及G1垃圾回收器爲基礎,梳理下調優思路、GC日誌如何閱讀及引起OOM的區域和緣由。html
ParNew通常用在新生代的垃圾回收器,CMS用在老年代的垃圾回收器,他們都是多線程併發機制,性能更好,如今通常是線上生產系統的標準組合。java
Minor GC又稱年輕代垃圾回收,年輕代垃圾回收主要採用複製算法,因爲年輕代對象大都「朝生夕死」,爲下降內存使用率瓶頸,設置了Eden區和2個Survior區,1個Eden區佔80%內存空間,每一塊Survivor區各佔10%內存空間。當前Minor GC主要採用ParNew垃圾回收器。程序員
新生代剩餘內存空間放不下新對象,此時須要觸發GC。算法
觸發Minor GC狀況有:數組
判斷老年代的可用內存是否已經小於了新生代的所有對象大小了,若是是,判斷-XX:HandlePromotionFailure參數是否設置,若是有這個參數,那麼就會繼續嘗試進行下一步判斷:看老年代的內存大小,是否大於以前每一次Minor GC後進入老年代的對象的平均大小。若是判斷失敗,或者空間分配擔保沒有設置,就會直接觸發一次FullGC,對老年代進行垃圾回收,儘可能騰出來一些空間,而後再執行Minor GC。bash
1.Minor GC事後,剩餘的存活對象,小於Survivor區域大小,存活對象進入Survivor區。多線程
2.Minor GC事後,存活對象大於Survivor區域大小,小於老年代可用空間大小,直接進入老年代併發
3.Minor GC事後,存活對象大於Survivor區域大小,也大於老年代可用空間大小,此時,就會發生Handle Promotionoracle
Old GC又稱老年代垃圾回收,針對老年代進行垃圾的回收器主要有Serial Old及CMS。若是Minor GC後存活對象大於老年代裏的剩餘空間,這個時候觸發一次Old GC, 將老年代裏的沒人引用的對象給回收掉,而後纔可能讓Minor GC事後剩餘的存活對象進入老年代裏面。app
當對象躲過15次Minor GC後、符合動態對象判斷規則、大對象及Minor GC後的對象太多沒法放入Survivor區域等場景,都會觸發對象進入老年代,下面將逐一分析每種場景。
不管15次GC以後進入老年代,仍是動態年齡判斷規則,都是但願可能長期存活的對象,儘早進入老年代。
這裏須要考慮一個問題,就是老年代空間不夠放這些對象。若是老年代的內存大小是大於新生代全部對象的,此時就能夠對新生代觸發一次Minor GC,由於即便全部對象都存活,Survivor區放不下了,也能夠轉移到老年代去。若是Minor GC前,發現老年代的可用內存已經小於新生代的所有大小了,這個時候若是Minor GC後新生代的對象所有存活下來,都轉移到老年代去,老年代空間不夠,理論上,是有這種可能的。因此假如Minor GC以前,發現老年代的可用內存已經小於了新生代的所有對象大小了,就會看一個-XX:HandlePromotionFailure的參數是否設置了。若是有這個參數,那麼就會繼續嘗試進行下一步判斷:看老年代的內存大小,是否大於以前每一次Minor GC後進入老年代的對象的平均大小。若是判斷失敗,或者空間分配擔保沒有設置,就會直接觸發一次FullGC,對老年代進行垃圾回收,儘可能騰出來一些空間,而後再執行Minor GC。
若是老年代回收後,仍然沒有足夠的空間存放Minor GC事後的剩餘存活對象,那麼此時就會致使OOM內存溢出
標記老年代當前存活對象,這些對象多是零散分佈在內存中,而後將這些存活對象在內存裏移動,將存活對象儘可能挪動到一邊,將存活對象集中放置,避免回收後出現過多內存碎片。而後一次行把垃圾對象都回收掉。
先經過追蹤GC Roots的方法,看看各個對象是否被GC Roots給引用了,若是是的話,那就是存活對象,不然就是垃圾對象。先將垃圾對象標記出來,而後一次性把垃圾對象都回收掉,這種方法其實最大的問題就是會形成不少內存碎片。
老年代存活對象太多了,若是採用複製算法,每次挪動可能90%的存活對象,這就不合適了。因此採用先把存活對象挪到一塊兒緊湊一些,而後回收垃圾對象的方式。
1.Minor GC以前,老年代內存空間小於歷次Minor GC後升入老年代對象的平均大小,判斷Minor GC有風險,可能就會提早觸發老年代GC回收老年代垃圾對象。
2.Minor GC後的對象太多了,都要升入老年代,發現空間不足,觸發一次老年代的Old GC。
3.設置了-XX:CMSInitiatingOccuancyFaction參數,好比設置爲92%,好比說老年代空間使用超過92%了,此時就會自行觸發Old GC.
CMS在執行一次垃圾回收的過程一共分爲4個階段。
標記出來全部GC Roots直接引用的對象,會讓系統的工做線程所有中止,進入「Stop the World」狀態。
追蹤老年代全部存活對象,老年代存活對象不少,這個過程就會很慢。
這個過程會標記整堆,包括年輕代和老年代。
找到零零散散分散再各個地方的垃圾對象,速度較慢。最後可能還要執行一次內存碎片整理,把大量的存活對象挪在一塊兒,空出來連續空間,這個過程仍然要STW,那就更慢了。
CMS垃圾收集器特有的錯誤,CMS的垃圾清理和引用線程是並行進行的,若是在並行清理的過程當中老年代的空間不足以容納應用產生的垃圾,則會拋出「concurrent mode failure」。
老年代的垃圾收集器從CMS退化爲Serial Old,全部應用線程被暫停,停頓時間變長。
緣由1:CMS觸發太晚
方案:將-XX:CMSInitiatingOccupancyFraction=N調小;
緣由2:空間碎片太多
方案:開啓空間碎片整理,並將空間碎片整理週期設置在合理範圍;
-XX:+UseCMSCompactAtFullCollection (空間碎片整理) -XX:CMSFullGCsBeforeCompaction=n,執行多少次Full GC以後再執行一次內存碎片整理工做,默認是0,意思就是每次Full GC以後都會進行一次內存整理。
新生代執行速度快,由於直接從GC Roots出發就追蹤哪些對象是活的便可,新生代存活對象是不多的,這個速度是很快的,不須要追蹤多少對象,最後直接把存活對象放入Survivor中,就一次性直接回收Eden和以前使用的Survivor了。
在老年代回收併發標記階段,他須要追蹤全部存活對象,老年代存活對象不少,這個過程就很慢。
從新標記這個過程要標記整堆,併發清理階段並非一次性回收一大片內存,而是找到零零散散在各個地方的垃圾對象,速度也很慢。
最後還須要執行一次內存碎片整理,把大量的存活對象給挪在一塊兒,空來聯繫內存空間,這個過程還得STW。
併發清理時,若是剩餘內存空間不足以存放要進入老年代的對象,會引起」Concurrent Mode Failure「問題,這時會採用」Serial Old「垃圾回收器,STW以後會重新進行一次Old GC,這就更耗時了。
JDK8後出現了G1垃圾回收器,經過-XX:+UseG1GC來指定G1垃圾回收器,是當下比較先進的垃圾回收器。G1能夠作到讓你來設定垃圾回收對系統的影響,他本身經過把內存拆分爲大量小Region,以及追蹤每一個Region中能夠回收的對象大小和預估時間,最後在垃圾回收的時候,儘可能把垃圾回收對系統形成的影響控制在你指定的時間範圍內,同時在有限的時間內儘可能回收儘量躲的垃圾對象。
G1垃圾回收器特色
新生代也是有eden和survivor劃分的,也是經過-XX:SurvivorRatio能夠劃分eden和survivor各自大小。觸發垃圾回收的機制也是相似的,隨着不停地在新生代eden對應的region中放對象,jvm會不停地給新生代加入更多的region,直到新生代佔堆大小的最大比例60%,好比說新生代1200個region了,裏面的eden可能佔據了1000個region,每一個survivor是100個region,並且eden區還佔滿了對象,這時會觸發新生代gc,g1採用以前說過的複製算法進行垃圾回收,進入一個STW狀態,併發eden對應的region中的存活對象放入S1的region中,接着回收掉eden對應的region中的垃圾對象。 g1是能夠設定目標gc停頓時間的,也就是g1執行gc的時候最多可讓系統停頓多長時間,能夠經過-XX:MaxGCPauseMills參數來設定,默認值是200ms。
對於G1內存模型來講,G1提供了專門的Region來存放大對象,而不是讓大對象進入老年代的Region中。在G1中,大對象的斷定規則就是一個大對象超過了一個region的50%,好比一個region是2MB,只要一個大對象超過了1MB,就會被放入大對象專門的region中,並且一個大對象若是太大,可能會橫跨多個region來存放。在新生代、老年代在回收的時候,會順帶着大對象一塊兒回收。
G1有一個參數,-XX:InitiatingHeapOccupancyPercent,默認值是45%,若是老年代佔據了堆內存45%的Region的時候,此時就會嘗試觸發一個新生代+老年代一塊兒回收的混合回收階段。
若是在進行Mixed回收的時候,不管是年輕代仍是老年代都基於複製算法進行回收,都要把各個Region的存活對象copy到別的Region裏去,萬一出現copy的過程當中發現沒有空閒Region能夠承載本身的存活對象了,就會觸發一次失敗。一旦失敗,立馬就會切換爲中止系統程序,而後採用單線程進行標記、清理和壓縮整理,空閒出來一批Region,這個過程是極慢極慢的。
老年代在堆內存裏佔比超過45%觸發mixed gc 優化的思路仍是儘可能避免對象過快進入老年代,儘可能避免頻繁觸發mixed gc。優化的核心點是:避免老年代達到InitiatingHeapOccupancyPercent設置的值,即避免對象過快進入老年代。
合理分配堆內存,經過調整s區和e區大小來控制進入老年代對象速度,從而減小頻繁old gc。
學會解讀gc日誌能夠很好地分析堆使用狀況,是進行調優及解決頻繁full gc必備技能。下面咱們以parnew+cms垃圾回收器爲例,分析下gc日誌。
public class JvmTest {
public static void main(String[] args) {
byte[] array1 = new byte[1024*1024];
array1 = new byte[1024*1024];
array1 = new byte[1024*1024];
array1 = null;
byte[] array2 = new byte[2*1024*1024];
}
}
複製代碼
Heap
par new generation total 4608K, used 2601K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)
eden space 4096K, 51% used [0x00000000ff600000, 0x00000000ff80a558, 0x00000000ffa00000)
from space 512K, 100% used [0x00000000ffa80000, 0x00000000ffb00000, 0x00000000ffb00000)
to space 512K, 0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)
concurrent mark-sweep generation total 5120K, used 62K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2782K, capacity 4486K, committed 4864K, reserved 1056768K class space used 300K, capacity 386K, committed 512K, reserved 1048576K
JVM退出時打印當前堆內存的使用狀況,分析以下:
Metaspace是從JVM進程的虛擬地址空間中分離出來的,用以保存類元數據。JVM在啓動時根據-XX:MetaspaceSize保留初始大小,該大小具備特定於平臺的默認值。
Metaspace由一個或多個虛擬空間組成。虛擬空間是由操做系統得到的連續地址空間。他們是按需分配的。在分配時,虛擬空間預留(reserves)了操做系統的內存,但尚未提交。Metaspace reserved是全部虛擬空間的總大小。虛擬空間中的分配單元是Metachunk,當從虛擬空間分配新塊時,相應的內存將committed, Metaspace committed是全部塊的總大小。 從 docs.oracle.com/javase/8/do… 中能夠對used,committed,reserved,capacity有了概述解釋;
In the line beginning with Metaspace, the used value is the amount of space used for loaded classes. The capacity value is the space available for metadata in currently allocated chunks. The committed value is the amount of space available for chunks. The reserved value is the amount of space reserved (but not necessarily committed) for metadata. The line beginning with class space line contains the corresponding values for the metadata for compressed class pointers.
Full GC有如下表象,如機器CPU負載太高,系統沒法處理請求或者處理過慢。引發Full GC的緣由有不少,主要有JVM參數設置不合理和代碼層面問題兩大類。JVM參數設置不合理,如新生代堆內存大小設置不合理、Eden與Survivor比例設置不合理,抑或是metaspace設置太小等。代碼層面問題,主要是程序員本身的問題,好比說對外提供查詢接口沒有作限制,一次查詢太多對象;應用中存在頻繁大量導出,且查詢沒有限制條件;代碼中顯示調用gc等。
public class FullGCTest {
public static void main(String[] args) {
byte[] array1 = new byte[4*1024*1024];
array1 = null;
byte[] array2 = new byte[2*1024*1024];
byte[] array3 = new byte[2*1024*1024];
byte[] array4 = new byte[2*1024*1024];
byte[] array5 = new byte[128*1024];
byte[] array6 = new byte[2*1024*1024];
}
}
複製代碼
結合上述配置,咱們能夠發現,數組array1這個大對象會直接進入老年代;以後連續分配了4個數組,其中3個是2MB的數組,1個是128KB的數組,會所有進入eden區。當再分配array6時,會發現eden區空間不夠,須要觸發一次minor gc,可是因爲array2,array3,array4,array5都被變量引用了,會直接進入老年代,由於老年代裏已經存在4MB的數據了,難以存放這麼大的數據,所以會觸發一次Full GC。Full GC會對老年代進行Old GC,同時通常會跟一次Young GC關聯,還會觸發一次Metaspace的GC。下面咱們分析下GC日誌。
0.308: [GC (Allocation Failure) 0.308: [ParNew (promotion failed): 7260K->7970K(9216K), 0.0048975 secs]0.314: [CMS: 8194K- >6836K(10240K), 0.0049920 secs] 11356K->6836K(19456K), [Metaspace: 2776K->2776K(1056768K)], 0.0106074 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
par new generation total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
concurrent mark-sweep generation total 10240K, used 6836K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
Metaspace used 2782K, capacity 4486K, committed 4864K, reserved 1056768K class space used 300K, capacity 386K, committed 512K, reserved 1048576K
下面分析full gc後堆內存的使用狀況
儘可能讓每次Young GC後的存活對象⼩於Survivor區域的50%,都留存在年輕代⾥。儘可能別讓對象進 ⼊⽼年代。儘可能減小Full GC的頻率,避免頻繁Full GC對JVM性能的影響。
系統通過單測、集測及測試環境後,進入預發環境進行壓測,觀察內存使用、Young GC的觸發頻率,Young GC的耗時,每次YoungGC後有多少對象是存活下來的,每次Young GC事後有多少對象進⼊了⽼年代,⽼年代對象增加的速率,Full GC的觸發頻率。
經過ps -ef | grep java獲取java進程pid,利用jstat工具查看gc狀況;
[tian~]$ jstat -gc 2236
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
20480.0 20480.0 269.9 0.0 163840.0 97683.3 319488.0 271892.4 673268.0 661182.8 78048.0 75954.8 508 9.526 18 1.737 11.263
複製代碼
S0C:這是From Survivor區的⼤⼩
S1C:這是To Survivor區的⼤⼩
S0U:這是From Survivor區當前使⽤的內存⼤⼩
S1U:這是To Survivor區當前使⽤的內存⼤⼩
EC:這是Eden區的⼤⼩
EU:這是Eden區當前使⽤的內存⼤⼩
OC:這是⽼年代的⼤⼩
OU:這是⽼年代當前使⽤的內存⼤⼩
MC:這是⽅法區(永久代、元數據區)的⼤⼩
MU:這是⽅法區(永久代、元數據區)的當前使⽤的內存⼤⼩
YGC:這是系統運⾏迄今爲⽌的Young GC次數
YGCT:這是Young GC的耗時
FGC:這是系統運⾏迄今爲⽌的Full GC次數
FGCT:這是Full GC的耗時
GCT:這是全部GC的總耗時
複製代碼
能夠利用jstat -gc PID 1000 10命令,每隔1s更新出來最新的一行jstat統計信息,一共執行10次統計,觀察每隔一段時間jvm中eden區對象佔用變化。若是系統訪問量較低,能夠適當延長觀察時間長度,這樣就能夠大體推測出每次gc停頓時間長度。如今也有比較好的可視化監測工具如JVisualVM和Cat等。
-Xmx8g -Xms8g -Xmn2g -Xss256k Xms、Xmx表示堆的大小,Xmn表示年輕代大小,Xss表示線程棧擦小,默認1M
-XX:SurvivorRatio=2 新生代中Eden與Survivor比值,調優的關鍵,也就是調節新生代堆大小及SurvivorRatio的值,儘可能讓新生代垃圾對象存放在Survivor中;
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=256m 元空間大小
-XX:+UseParNewGC 用並行收集器 ParNew 對新生代進行垃圾回收
-XX:+UseConcMarkSweepGC 併發標記清除收集器 CMS 對老年代進行垃圾回收。
-XX:ParallelGCThreads=2 Young GC工做時的並行線程數
-XX:ParallelCMSThreads=3 CMS GC 工做時的並行線程數
-XX:+CMSParallelRemarkEnabled 並行運行最終標記階段,加快最終標記的速度
-XX:+CMSParallelInitialMarkEnabled 初始階段開啓多線程併發執行,減小STW時間
-XX:+CMSScavengeBeforeRemark 在CMS從新標記階段以前,執行一次Young GC,由於從新標記是整堆標記的,執行一次Young GC,回收調年輕代裏沒人引用的對象,減小掃描對象。
-XX:MaxTenuringThreshold=15 對象重新生代晉升到老年代的年齡閾值(每次 Young GC 留下來的對象年齡加一),默認值15
-XX:+UseCMSCompactAtFullCollection 開啓碎片整理
-XX:CMSFullGCsBeforeCompaction=2 與-XX:+UseCMSCompactAtFullCollection配合使用,表示進行2次Full GC後進行整理
-XX:+UseCMSInitiatingOccupancyOnly 只根據老年代使用比例來決定是否進行CMS
-XX:CMSInitiatingOccupancyFraction=80 設置觸發CMS老年代回收的內存使用率佔比,達到80%時觸發old gc
-XX:+CMSClassUnloadingEnabled 默認開啓,表示開啓 CMS 對元空間的垃圾回收,避免因爲元空間耗盡帶來 Full GC
-XX:-DisableExplicitGC 禁止代碼中顯示調用GC
-XX:+HeapDumpOnOutOfMemoryError OOM時dump內存快照
-verbose:gc 表示輸出虛擬機中GC的詳細狀況
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:/app/log/xxx.log gc文件未知
複製代碼
發生OOM的區域主要有三塊,一個Metaspace區域,一個是虛擬機棧內存,一個是堆內存空間。
Full GC時,必然會嘗試回收Metaspace區域中的類,固然回收條件是比較苛刻的,如這個類的類加載器先要被回收,類的全部對象實例都要被回收等,一旦Metaspace區域滿類,未必能回收掉裏面不少的類,JVM沒有回收太多空間,隨着程序運行,還要繼續往Metaspace區域中塞入更多的類,直接就會引起內存溢出問題。 引發Metaspace內存溢出的緣由
每一個線程的虛擬機棧的大小是固定的,線程調用一個方法,都會將本次方法調用的棧楨壓入虛擬機棧裏,這個棧枕裏是有方法的局部變量的。致使棧內存溢出的主要緣由是出現類遞歸調用。
堆內存溢出主要是eden區不斷有存活對象進入老年代,觸發full gc後發現老年代回收對象較少,老年代仍然有大量存活對象,年輕代仍然有一批對象等着放進老年代,可是放不下,這時候拋出內存溢出異常。 通常來講,引發內存溢出主要有兩種場景: