前序文章:
JVM性能調優(1) —— JVM記憶體模型和類加載運行機制
JVM性能調優(2) —— 垃圾回收器和回收策略
一、記憶體調優的目標
新生代的垃圾回收是比較簡單的,Eden區滿了無法分配新物件時就觸發 YoungGC,而且新生代采用的復制演算法效率極高,加上新生代存活的物件很少,只要迅速標記出這少量存活物件,移動到Survivor區,然后快速回收掉Eden區,速度很快,一般一次YoungGC就耗費幾毫秒或幾十毫秒,所以新生代GC對系統的影響基本不是很大,
但老年代的GC就不一樣了,老年代GC通常都很耗費時間,尤其是頻繁觸發老年代GC(FullGC/OldGC),因為無論是CMS垃圾回收器還是G1垃圾回收器,比如說CMS就要經歷初始標記、并發標記、重新標記、并發清理、碎片整理幾個環節,程序非常的復雜,STW的時間也會更長,G1同樣也是如此,通常來說,FullGC至少比YoungGC慢10倍以上,
新生代物件進入老年代有四個時機:物件年齡超過閥值、大物件直接進入老年代,動態年齡判斷規則、新生代GC后存活物件太多無法放入Survivor區,物件年齡太大進入老年代無可避免,因為這部分物件一般來說都是長期存活的物件,是需要進入老年代的,而后三個一般都是因為記憶體分配不合理或一些引數設定不合理導致物件進入老年代,而且基本都是生命周期較短的物件,然后占滿老年代,觸發老年代GC,
因此,基于JVM運行的系統最大的問題,就是因為記憶體分配、引數設定不合理,導致物件頻繁的進入老年代,然后頻繁觸發FullGC,導致系統每隔一段時間就卡頓幾百毫秒甚至幾秒鐘,這對用戶體驗來說將是極差的,
所以,JVM調優的目標,最重要的就是對記憶體分配調優,然后合理優化新生代、老年代、Eden和Survivor各個區域的記憶體大小,接著再盡量優化引數避免新生代的物件進入老年代,盡量讓物件留在新生代里被回收掉,甚至不會出現 FullGC,
二、估算記憶體運轉模型
在設定JVM記憶體的時候,是沒有一個固定標準、固定引數的,但是有一套比較通用的分析和優化方法,就是根據實際業務預估這個系統未來的業務量、訪問量,去推算這個系統每秒種的并發量,然后推算每秒鐘的請求對記憶體空間的占用,進而推算出整個系統運行期間的JVM記憶體運轉模型,然后通過各個引數調優,盡量讓垃圾物件在年輕代被回收掉,避免頻繁 Full GC,
下面就假定有一個每日百萬交易的支付系統,來看看怎么估算一個比較合理的記憶體運轉模型,
第1步:分析系統核心業務與核心壓力
首先要分析出一個系統的核心壓力集中在哪里,每日百萬交易的支付系統,最核心的業務當屬支付流程,每次支付請求將創建至少一個訂單物件,這個訂單物件包含支付的用戶、渠道、金額、商品、時間等資訊,
支付系統的壓力有很多方面,包括高并發請求、高性能處理請求、大量訂單資料存盤等,但在JVM層面,這個支付系統最大的壓力就是每天會在JVM中頻繁的創建和銷毀100萬個支付訂單物件,
第2步:預估每秒需處理多少次請求
要設定合理的JVM記憶體大小,首先要估算出核心業務每秒鐘有多少次請求,假設每天100萬個支付訂單,一般用戶交易都集中在每天的高峰期,也就是中午或晚上那3~4個小時,那么平均每秒就將近100次,
假設支付系統部署3臺機器,那么平均到每臺機器就30個支付請求,
第3步:估算一次請求耗時多久
用戶發起一次支付請求,后端將創建一個訂單物件、做一些關聯校驗、寫入資料庫等,還有一些其它操作,比如呼叫第三方支付平臺等,假設一次支付請求耗時1秒吧,那么每秒鐘就會產生30個訂單物件,然后1秒后這30個物件就變為垃圾物件了,
第4步:估算每秒請求占多少記憶體
我們可以根據訂單類中的實體變數型別來計算就可以了,比如 Integer 占4個位元組,Long 占8個位元組,String 型別根據長度來計算,假設一個訂單類按20個欄位來算,往大一點粗略估算占500位元組吧,那么每秒30個支付請求就是 30 * 500B ≈ 15KB,
但實際上,每次請求的程序中,除了訂單物件,往往還會創建大量其它型別的物件,比如其它的一些關聯查詢物件,Spring框架創建的物件等,這時一般需要對單個物件放大10~20倍,
而且支付系統還會包含其它的一些業務,比如交易記錄、對賬管理、結算管理等,再擴大個5~10倍,這樣算下來每秒鐘基本會產生1M左右的物件,
但這些也不是絕對的,對于一些特殊的系統,比如報表系統、資料計算系統,每次請求創建的物件可能超過10幾M了,那么附屬創建的這些物件可能影響就沒那么大了,此時可以考慮忽略不計,
第5步:估算元空間大小
元空間主要是存放型別資訊,也沒什么太多好調優的,一般設定幾百M夠用就可以了,比如256M,
第6步:估算堆疊記憶體大小
執行緒堆疊主要就是運行期間存盤方法的引數、區域變數等資訊,一般設定1M就足夠了,比如系統有100個執行緒,那么虛擬機堆疊就會至少占用100M記憶體,
第7步:記憶體分配
這個每日百萬交易的支付系統部署3臺機器,每臺機器每秒扛30個請求,假設部署的機器是2核4G,但是機器本身運行還需要一些記憶體,那么JVM就只分2G,考慮到要給元空間、虛擬機堆疊預留空間,那假設堆記憶體只分1G,新生代給500M,老年代給500M,那 Eden 區就占400M,兩個 Survivor 區各占50M,
這樣估算下來,就是如下的記憶體引數設定:
-Xms1G -Xmx1G -Xmn500M -Xss1M -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:SurvivorRatio=8
第8步:系統運轉模型
經過上面的分析,再結合機器配置,我們就能大致估算這個系統的記憶體運轉模型了,使用上面的記憶體設定,那么每秒接收30個請求,在Eden區創建30個訂單相關的物件;將產生1M新物件,1秒后請求處理完成,將產生1M的垃圾物件;將在400秒后,也就幾分鐘的時間,Eden 區就占滿了,然后觸發 Young GC;YoungGC時會把存活物件復制到FromSurvivor區,然后回收掉新生代的垃圾物件,如此往復,如果Survivor區分配不合理,導致存活物件進入老年代,還可以估算出多久觸發一次FullGC/OldGC,主要就是估算出GC的頻率,然后就可以對記憶體進行調優了,
第9步:瞬時壓力增加時的模型估算
如果遇到搞大促活動或一些突發的性能抖動,壓力可能瞬間增加10倍甚至更多,那每秒可能就是上千筆支付請求,每秒記憶體占用至少10M以上了,這個時候每次支付請求可能就不是1秒能處理完的了,因為壓力驟增,系統記憶體、執行緒資源、CPU資源都將打滿,導致系統性能下降,這樣可能有些支付請求需要耗時好幾秒,那可能就有幾十M物件會占用堆記憶體幾秒鐘,
還是按照2核4G的機器部署,堆記憶體設定1G,新生代500M,Eden區400M,Survivor50M,這時Eden區只需幾十秒就滿了,然后觸發YoungGC,但是,因為壓力增加,有些請求需要好幾秒,就會有幾十M物件會將無法被回收,就被復制到 Survivor 區,
這時就有多種情況了,首先存活幾十M的物件可能大于Survivor區50M的記憶體,那么就會直接復制到老年代,然后如果小于Survivor區,也大于了Survivor區50%的空間了,下一次通過動態年齡規則判斷也可能會將部分物件復制到老年代,
然后經過大概10幾次YoungGC,也就幾百秒后老年代也快滿了,這時可能就會觸發FullGC,FullGC時要暫停系統運行,無法處理任何請求,而且這種情況下老年代大部分都是垃圾物件,回收性能是很低的,
三、YoungGC 調優
1、合理分配記憶體降低YoungGC頻率
根據前面的估算,在正常的情況下如果給堆分配1G的空間,會頻繁觸發 YoungGC,新生代回收雖然效率高,但也會 Stop The World,暫停系統運行,如果頻繁YoungGC,就會頻繁暫停系統,
我們可以考慮增大新生代記憶體,同時使用記憶體大一點的機器,比如使用4核8G,那么JVM分4G,給堆空間分配3G,新生代給1.5G,老年代給1.5G,Eden 區差不多1.2G,Survivor區150M,這個時候Eden區差不多要半個小時才會占滿,然后觸發一次YoungGC,而其中99%都是垃圾物件,采用標記-復制演算法基本上很能就能完成YoungGC,這就大大降低了YoungGC的頻率,
如果業務量更大,還可以考慮橫向多部署幾臺機器,這樣分到每臺機器的請求就更少了,壓力也更小,
2、保證Survivor空間足夠
如果遇到大促活動,瞬時壓力增大,每秒就會有10M以上的物件產生,然后有幾十兆甚至上百兆的物件會存活幾秒以上,按照前面的記憶體模型來分析下,那 Eden 區2分鐘左右就會占滿,然后將存活的幾十兆物件復制到 Survivor 區;如果這批存活物件大于150M,將直接進入老年代;如果小于150M但大于 75M,那么由于動態年齡判斷也有可能頻繁導致部分生命周期短的物件進入老年代,老年代如果快速占滿將頻繁觸發FullGC,
新生代調優最重要的一個就是盡量保證 Surivivor 空間足夠,避免因為 YoungGC 時Survivor空間不夠導致大批物件進入老年代,這樣就能極大減少甚至不會FullGC了,
這種業務系統其實絕大多數物件的生命周期都很短,長時間存活的物件占不了多少記憶體,我們應該盡量讓物件都留在新生代里,因此我們可以把新生代的記憶體占比調高一點,比如新生代給2G,老年代給1G,這樣 Eden 區就占了1.6G,Survivor 占200M,這樣就基本能保證每次YoungGC時存活的物件都能放進 Survivor 區了,或者再可以用 -XX:SurvivorRatio 引數調整下 Eden 區和 Survivor 區的比例,讓 Survivor 區盡可能裝下每次 YoungGC 后存活的物件,
3、優化物件年齡閥值
還有一種情況會導致新生代物件進入老年代,就是有些物件連續躲過15次回收后,就會晉升到老年代,這個我們也可以結合實際的業務模型做調整,比如大促的場景中,新生代分2G,Eden區分1.6G,差不多每隔3分鐘就觸發一次YoungGC,那么在新生代來回復制15次就是45分鐘左右的時間才會進入老年代,對于這個系統來說,絕大多數物件的生命周期都是很短的,能存活幾分鐘以上的物件應該都是程式中的 Controller、Service、Repository 之類的需要長期存活的業務核心組件,
所以對于這種型別的系統,應盡快讓長期存活的物件進入老年代,而不是在新生代來回復制15次后再進入老年代,可以通過 -XX:MaxTenuringThreshold 引數降低年齡閥值,比如設定為 5,
4、優化大物件閥值
還有一種情況就是大物件將直接進入老年代,大物件閥值一般設定1M就夠了,一般來說很少有一個物件超過1M的,如果我們確定系統中會頻繁創建生命周期短的大物件,我們可以適當調大這個閥值,避免其進入老年代,
可以通過引數 -XX:PretenureSizeThreshold=1M 來設定大物件閥值,
5、選擇垃圾回收器
新生代垃圾回收器有 Serial、ParNew、ParallelScavenge,一般來說老年代要用性能較好的 CMS 垃圾回收器,那么新生代就只能指定 ParNew 回收器,
使用 ParNew 回收器,調優的思路基本就是前面4點,合理分配新生代記憶體,保證物件能放入 Survivor 區,避免進入老年代,基本 YoungGC 就沒啥問題了,
6、JVM引數
調優后的JVM引數如下:
-Xms3G -Xmx3G -Xmn2G -Xss1M -XX:SurvivorRatio=8 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC
四、FullGC 調優
老年代主要使用CMS垃圾回收器,我們就主要結合上面的業務模型來看看CMS回收器的各個引數優化,
1、多久觸發一次 FullGC
在前面年輕代的優化基礎之上,我們還需要估算系統多久會觸發一次 Full GC,這將決定我們是否要重點優化下老年代,比如估算下來每隔一兩個小時或更久執行一次 Full GC,這時候高峰期那一個小時已經過了,這時候執行 Full GC 對系統的影響來說其實是很小的了,
首先看下觸發 Full GC 的條件:
- ① JDK6 之前有個 -XX:HandlePromotionFailure 分配擔保失敗的引數,就是每次 YoungGC 前都會判斷老年代的可用空間大小是否大于新生代物件總大小,按前面的配置,新生代最多會有 1.8G 的物件,老年大最大才 1G,那豈不是每次 YoungGC 都會擔保失敗,不過JDK1.6之后就沒有這個引數了,也沒有這個判斷了,
- ② 每次 YoungGC 前檢查老年代可用空間是否大于歷次YoungGC后進入老年代的平均物件大小,按照前面的配置,基本上物件在新生代就被回收了,歷次進入老年代的平均物件大小其實是很小的,這個條件基本不會觸發,
- ③ 可能某次 YoungGC 后存活物件大于 Survivor 區大小了,要復制到老年代,但發現老年代空間不足也放不下了,這時就會觸發FullGC,但年輕代優化好之后,這種概率是非常小的了,
- ④ CMS 有個 92% 的閥值,就是老年代超過 92% 的時候,會自動觸發老年代垃圾回收,這個引數可以通過 -XX:CMSInitiatingOccupancyFraction 設定,
系統運行時,可能會有部分物件慢慢進入老年代,但是新生代優化好之后,物件晉升到老年代的速度是很慢的,可能需要幾個小時才觸發一次 FullGC,錯過高峰期,FullGC 的影響也不會太大,
2、CMS并發失敗
觸發老年代GC后,基本就是老年代快滿了,CMS有個92%的閥值,那么1G的老年代,就還剩100M左右空間,如果老年代在并發回收時,新晉升到老年代的物件超過100M了,就會導致并發失敗(Concurrent Model Failure),并發失敗后,就會進入 Stop The World 的狀態,老年代切換為 Serial Old 回收器,Serial Old 回收器是單執行緒回收,效率非常低的,
但是經過年輕代的調優后,物件升入老年代的速度是很慢的,而且每次升入老年代的平均物件大小是很小的,所以一般在并發回收時還有超過100M的物件升入老年代的概率也是很小的,這種情況下我們一般也不用去調整 -XX:CMSInitiatingOccupancyFraction 引數的值,
3、CMS回收后碎片整理頻率
CMS完成FullGC后,默認是每次都會進行一次記憶體碎片整理,這個程序也會 Stop The World,但是按照前面的分析,其實我們也沒必須要調整這部分引數,
CMS 通過 -XX:+UseCMSCompactAtFullCollection 引數開啟GC后記憶體碎片整理的程序,通過 -XX:CMSFullGCsBeforeCompaction 設定多少次FullGC后進行記憶體碎片整理,默認0,就是每次FullGC后都整理,
一般不用調整 CMSFullGCsBeforeCompaction 的值,提高這個值,意味著要多次 FullGC 后才會進行記憶體碎片整理,那么前幾次FullGC會導致很多記憶體碎片產生,不整理就會導致更頻繁的觸發FullGC,因為雖然FullGC后可用空間很多,但可用的連續空間并不多,所以一般是設定為0,每次FullGC后整理記憶體碎片,
4、CMS提升FullGC的性能
CMS還有兩個引數可以進一步優化FullGC的性能,降低FullGC的時間,
-XX:+CMSParallelInitialMarkEnabled:開啟這個引數會在CMS垃圾回收器的“初始標記”階段開啟多執行緒并發執行,減少STW的時間,進一步降低FullGC的時間,
-XX:+CMSScavengeBeforeRemark:這個引數會在CMS的重新標記階段之前,先盡量執行一次YoungGC,CMS的重新標記也會STW,所以如果在重新標記之前,先執行一次YoungGC,就會回收掉一些年輕代里沒有被參考的物件,那么在CMS的重新標記階段就可以少掃描一些物件,此時就可以提升CMS的重新標記階段的性能,減少這個階段的耗時,(注意:無論是并發標記還是重新標記,都會掃描整個堆的物件,因為就算物件在老年代,也可能被新生代物件參考著)
5、禁用System.gc
在代碼中,我們可以通過 System.gc() 建議JVM執行一次 FullGC,但JVM不一定會執行,但這個方法不能隨便呼叫,基本上來說是禁止手動 GC 的,因為使用不當很有可能會頻繁觸發 FullGC,
針對這個,我們一般可以通過加入 -XX:+DisableExplicitGC 引數來禁止顯示執行GC,就是不允許通過代碼 System.gc 來觸發GC,
6、元空間優化
FullGC 不只老年代滿了會觸發,元空間配置不當或動態加載的類過多也有可能頻繁觸發 FullGC,
一般可能有如下情況會動態生成類放入Metaspace區域:
- 比如通過 ASM、CGLib、javassist 等位元組碼框架創建代理類
- 還有通過反射呼叫時,如 Method method = XXX.class.getDeclaredMethod(); method.invoke(target, args);,在反射呼叫一定次數后就會動態生成一些類
如果由于元空間導致了 FullGC,我們可以加上 -XX:+TraceClassLoading、-XX:+TraceClassUnloading 來觀察有哪些類頻繁的被加載和卸載,然后分析出根源問題,
有兩個引數可控制元空間的大小:
- -XX:MaxMetaspaceSize:設定元空間最大值,默認是 -1,即不限制,只受限于本地記憶體大小
- -XX:MetaspaceSize:指定元空間的初始空間大小,達到該值就會觸發垃圾回收進行型別卸載,同時收集器會對該值進行調整:如果釋放了大量的空間,就適當降低該值;如果釋放了很少的空間,那么在不超過 -XX:MaxMetaspaceSize的情況下,適當提高該值,
7、JVM引數
-Xms3G -Xmx3G -Xmn2G -Xss1M -XX:SurvivorRatio=8 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92
-XX:CMSWaitDuration=2000
-XX:+UseCMSInitiatingOccupancyOnly -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSParallelInitialMarkEnabled
-XX:+CMSScavengeBeforeRemark
-XX:+DisableExplicitGC
五、大記憶體機器GC調優
1、使用大記憶體機器的場景
前面通過對支付系統的優化,YoungGC 的頻率為幾分鐘一次,Full GC 基本不會發生,但是像遇到雙十一這樣的大促場景,可能就凌晨那幾分鐘就會增加平時數十倍甚至上百倍的壓力,這個時候如果還是按照4核8G的記憶體來部署,那可能需要上百臺機器,這個時候就可以考慮提升機器的配置,比如提升到16核32G,每臺機器每秒可以扛幾千次請求,這樣就只需要部署十多臺機器可能就夠了,
其實還有類系統比如報表系統、BI系統、資料計算系統、大資料系統,這類系統的核心業務如資料報表,一次請求可能會查詢幾十上百兆資料在記憶體中做計算,如果還是使用小記憶體機器,那么Eden區將迅速填滿,然后觸發 YoungGC,而且隨著并發壓力增加,需要加更多機器,這種情況下我們一般就可以提高機器配置,使用大記憶體機器來部署了,
總的來說使用大記憶體機器的場景一般就是由于并發量高或每次請求記憶體占用高導致頻繁YoungGC,然后需要增加很多臺機器的時候,為了減少機器的數量,我們就可以使用大記憶體機器來部署,
2、大記憶體機器的問題
比如使用16核32G的記憶體,假設新生代給20G,那么Eden區就是16G,Survivor 區各占2G,按每秒產生50M物件來計算,5分鐘左右就會觸發一次YoungGC,記憶體比之前擴大了10倍,這時如果還是使用 ParNew+CMS這樣的垃圾回收器組合,YoungGC 的停頓時間就需要幾百毫秒甚至一兩秒,這個時候就是每隔幾分鐘卡個幾百毫秒,而且由于長時間卡頓,還會導致請求積壓排隊,嚴重的時候還會導致有些請求超時回傳,如果再提高配置,比如使用32核64G,那每次YoungGC就需要停頓幾秒鐘了,這對系統的影響就非常大了,
這個時候就可以使用G1回收器來解決大記憶體YoungGC過慢的問題,我們可以給G1設定一個預期的GC停頓時間,比如100毫秒,這樣G1會保證每次YoungGC停頓時間不超過100毫秒,避免影響用戶的體驗,
不過對于一些后臺運行不直接面向用戶的系統,就算一次GC耗時1秒或幾秒其實影響也不大,這個時候就沒必要用G1回收器了,
3、G1回收器調優
1)G1記憶體布局
G1 可以使用 -XX:G1NewSizePercent 設定新生代Region初始占比,默認是5%;使用 -XX:G1MaxNewSizePercent 設定新生代Region最大占比,默認是 60%,這兩個引數一般不用去設定,使用默認值就可以了,
默認情況下,G1 每個 Region 大小為堆記憶體大小除以2048,取2的N次冥,也可以通過 -XX:G1HeapRegionSize 引數設定每個 Region 的大小,
2) GC停頓時間
G1 有一個非常重要的引數會影響到G1回收器的表現:-XX:MaxGCPauseMillis,用來設定一次GC最大的停頓時間,這個引數一般需要結合系統壓測工具、GC日志、記憶體分析工具來綜合參考,要盡量讓GC的頻率別太高,同時每次GC停頓時間也別太長,達到一個理想的合理值,
G1會隨著系統的運行,不斷給新生代分配Region,但并不是非要到60%時才觸發YoungGC,其實G1到底會分配多少個Region給新生代,多久觸發一次YoungGC,每次耗費多長時間,這些都是不確定的,它整個都是動態的,它會根據預設的停頓時間,給新生代分配一些記憶體,然后到一定程度就觸發YoungGC,把GC時間控制在預設的時間內,避免一次回收過多的Region導致GC停頓時間超出預期,又避免一次回收過少的Region導致頻繁GC,
3)MixedGC 優化
G1 默認在老年代占比超過45%時,就會觸發 MixedGC,其實優化 MixedGC 最重要的還是優化記憶體分配,盡量避免物件進入老年代,盡量避免頻繁觸發 MixedGC 就行了,
然后還是最核心的 -XX:MaxGCPauseMillis 引數,如果這個引數設定過高,導致系統運行很久,然后新生代占比達到60%了,這個時候可能存活下來的物件放不進Survivor區或者觸發Survivor區動態年齡判斷,就會導致有些物件進入老年代,進而觸發MixedGC,所以就需要合理設定這個引數,保證YoungGC別太頻繁的同時,還得考慮每次GC過后存活的物件大小,避免大量物件進入老年代而觸發 MixedGC,
4)JVM引數
-Xms24G -Xmx24G -Xmn20G -Xss1M -XX:SurvivorRatio=8 -XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M -XX:MaxTenuringThreshold=5 -XX:PretenureSizeThreshold=1M -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:G1NewSizePercent=5 -XX:G1MaxNewSizePercent=60 -XX:G1HeapRegionSize=4M -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=4
六、OOM記憶體溢位問題
在《Java虛擬機規范》的規定里,除了程式計數器外,虛擬機記憶體的其他幾個運行時區域都有發生OutOfMemoryError(OOM)例外的可能,通常而言,記憶體溢位問題對系統是毀滅性的,它代表VM記憶體不足以支撐程式的運行,所以—旦發生這個情況,就會導致系統直接停止運轉,甚至會導致VM行程直接崩潰掉,OOM是非常嚴重的問題,這節就來看下通常有哪些原因導致OOM,
1、元空間溢位
1)元空間溢位原因
Metaspace 這塊區域一般很少發生記憶體溢位,如果發生記憶體溢位—般都是因為兩個原因:
- Metaspace 引數設定不當,比如 Metaspace 記憶體給的太小,就很容易導致 Metaspace 不夠用
- 代碼中用 CGLib、ASM、javassist 等動態位元組碼技術動態創建一些類,如果代碼寫的有問題就可能導致生成過多的類而把 Metaspace 塞滿
2)模擬元空間溢位
下面通過CGLib來不斷創建類來模擬塞滿 Metaspace,
首先在 pom.xml 添加 cglib 的依賴:
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.2.4</version> </dependency>
下面這段程式通過CGLib不斷地創建代理類:
1 public class GCMain { 2 3 public static void main(String[] args) { 4 while (true) { 5 Enhancer enhancer = new Enhancer(); 6 enhancer.setSuperclass(IService.class); 7 enhancer.setUseCache(false); 8 enhancer.setCallback(new MethodInterceptor() { 9 @Override 10 public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { 11 return methodProxy.invokeSuper(o, objects); 12 } 13 }); 14 enhancer.create(); 15 } 16 } 17 18 static class IService { } 19 }
設定如下的JVM引數:元空間固定10M,還添加了追蹤類加載和卸載的引數
-Xms200M -Xmx200M -Xmn150M -XX:SurvivorRatio=8 -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M -XX:+UseConcMarkSweepGC -XX:+TraceClassLoading -XX:+TraceClassUnloading -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:./gc.log
運行程式一會就報OOM錯誤,然后直接退出運行,
從 Caused by: java.lang.OutOfMemoryError: Metaspace 可以看出是由于 Metaspace 引起的OOM,而且從上面類加載的追蹤可以看到,程式一直在加載CGLIB動態創建的代理類,

再看下GC日志:可以看出由于元空間滿了觸發了一次 FullGC,

2、堆疊溢位
1)堆疊溢位原因
通過前兩篇文章可以知道,每個執行緒都會有一個執行緒堆疊,執行緒堆疊的大小是固定的,比如設定的1MB,這個執行緒每呼叫一個方法,都會將呼叫方法的堆疊楨壓入執行緒堆疊里,方法呼叫結束就彈出堆疊幀,堆疊楨會存盤方法的區域變數、例外表、方法地址等資訊,也是會占用一定記憶體的,
如果這個執行緒不停的呼叫方法,不停的壓入堆疊幀,而沒有彈出堆疊幀,比如遞回呼叫沒有寫好結束條件,那執行緒堆疊遲早都會被占滿,然后導致堆疊記憶體溢位,一般來說,引發堆疊記憶體溢位,往往都是代碼里寫了一些bug導致的,正常情況下很少發生,
關于虛擬機堆疊和本地方法堆疊,《Java虛擬機規范》中描述了兩種例外:StackOverflowError 和 OutOfMemoryError,
① StackOverflowError
如果執行緒請求的堆疊深度大于虛擬機所允許的最大深度,將拋出 StackOverflowError 例外,堆疊深度在大多數情況下到達1000~2000是完全沒有問題,對于正常的方法呼叫,這個深度應該完全夠用了,
② OutOfMemoryError
如果虛擬機的堆疊記憶體允許動態擴展,當擴展堆疊容量無法申請到足夠的記憶體時,將拋出 OutOfMemoryError 例外,而HotSpot虛擬機是不支持擴展的,而且堆疊深度是動態變化的,在設定執行緒堆疊大小時(-Xss),如果設定小一些,相應的堆疊深度就會縮小,
所以 HotSpot 虛擬機堆疊溢位只會因為堆疊容量無法容納新的堆疊幀而導致 StackOverflowError 例外,而不會出現 OutOfMemoryError 例外,
2)模擬堆疊溢位
運行如下這段代碼:遞回呼叫 recursion 方法,沒有結束條件,所以必定會導致堆疊溢位
1 public class GCMain { 2 3 public static void main(String[] args) { 4 recursion(1); 5 } 6 7 public static void recursion(int count) { 8 System.out.println("times: " + count++); 9 recursion(count); 10 } 11 }
設定如下JVM引數:執行緒堆疊設定為256K
-Xms200M
-Xmx200M
-Xmn150M
-Xss256K
-XX:SurvivorRatio=8
-XX:MetaspaceSize=10M
-XX:MaxMetaspaceSize=10M
運行一會就出現了 StackOverflowError 例外:

3、堆溢位
1)堆溢位原因
堆記憶體溢位主要就是因為有限的記憶體中放了過多的物件,而且大多數都是存活的,即使GC過后還是大部分都存活,然后堆記憶體無法在放入物件就導致堆記憶體溢位,
—般來說堆記憶體溢位有兩種主要的場景:
- 系統負載過高,請求量過大,導致大量物件都是存活的,無法繼續放入物件后,就會引發OOM系統崩潰
- 系統有記憶體泄漏的問題,莫名其妙創建了很多的物件,而且都是存活的,GC時無法回收,最終導致OOM
2)模擬堆溢位
運行如下代碼:不斷的創建 String 物件,而且都被 datas 參考著無法被回收掉,最終必然會導致OOM,
1 public static void main(String[] args) { 2 Set<String> datas = new HashSet<>(); 3 while (true) { 4 datas.add(UUID.randomUUID().toString()); 5 } 6 }
設定如下JVM引數:新生代、老年代各100M
-Xms200M -Xmx200M -Xmn100M -Xss1M -XX:SurvivorRatio=8 -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M -XX:+UseParNewGC
OutOfMemoryError:可以看到由于Java heap space 不夠了導致OOM,

4、OOM問題如何解決
要解決OOM問題,首先要知道什么物件太多導致OOM的,這就需要在OOM時dump記憶體快照,
只需要加入如下啟動引數就可以在OOM時自動dump記憶體快照:
- -XX:+HeapDumpOnOutOfMemoryError:OOM時自動dump記憶體快照
- -XX:HeapDumpPath=dump.hprof:快照檔案存盤位置
有了記憶體快照后就可以使用 MAT 這類工具來分析大量創建了哪些物件,
七、性能調優總結
1、調優程序總結
一般來說GC頻率是越少越好,YoungGC的效率很快,FullGC則至少慢10倍以上,所以應盡可能讓物件在年輕代回收掉,減少FullGC的頻率,一般一天只發生幾次FullGC或者幾天發生一次,甚至不發生FullGC才是一個比較良好的JVM性能,
從前面的調優程序可以總結出來,老年代調優的前提是年輕代調優,年輕代調優的前提是合理分配記憶體空間,合理分配記憶體空間的前提就是估算記憶體使用模型,
因此JVM調優的大致思路就是先估算記憶體使用模型,合理分配各代的記憶體空間和比例,盡量讓年輕代存活物件進入Survivor區,讓垃圾物件在年輕代被回收掉,不要進入老年代,減少 FullGC 的頻率,最后就是選擇合適的垃圾回收器,

2、頻繁FullGC的幾種表現
當出現如下情況時,我們就要考慮是不是出現頻繁的FullGC了:
- 機器 CPU 負載過高
- 頻繁 FullGC 報警
- 系統無法處理請求或者處理過慢
CPU負載過高一般就兩個場景:
- 在系統里創建了大量的執行緒,這些執行緒同時并發運行,而且作業負載都很重,過多的執行緒同時并發運行就會導致機器CPU負載過高,
- 機器上運行的VM在執行頻繁的FullGC,FullGC是非常耗費CPU資源的,而且頻繁的FullGC會導致系統時不時的卡死,
3、頻繁FullGC的幾種常見原因
① 系統承載高并發請求,或者處理資料量過大,導致YoungGC很頻繁,而且每次YoungGC過后存活物件太多,記憶體分配不合理,Survivor區域過小,導致物件頻繁進入老年代,頻繁觸發FullGC
② 系統一次性加載過多資料進記憶體,搞出來很多大物件,導致頻繁有大物件進入老年代,然后頻繁觸發FullGC
③ 系統發生了記憶體泄漏,創建大量的物件,始終無法回收,一直占用在老年代里,必然頻繁觸發FullGC
④ Metaspace 因為加載類過多觸發FullGC
⑤ 誤呼叫 System.gc() 觸發 FullGC
4、JVM引數模板
通過前面的分析總結,JVM引數雖然沒有固定的標準,但對于一般的系統,我們其實可以總結出一套通用的JVM引數模板,基本上保證JVM的性能不會太差,又不用一個個系統去調優,在某個系統遇到性能問題時,再針對性的去調優就可以了,
對于一般的系統,我們可能使用4核8G的機器來部署,那么總結一套模板如下:
- 堆記憶體分配4G,新生代3G,老年代1G,Eden區2.4G,Survivor區各300M,一般來說YoungGC后存活的物件小于150M就沒太大問題
- 元空間給個 512M 一般就足夠了,如果系統會運行時創建很多類,可以調大這個值
- -XX:CMSFullGCsBeforeCompaction設定為0,每次FullGC后都進行一次記憶體碎片整理
- -XX:+CMSParallelInitialMarkEnabled,CMS初始標記階段開啟多執行緒并發執行,降低FullGC的時間
- -XX:+CMSScavengeBeforeRemark,CMS重新標記階段之前,先盡量執行一次Young GC
- -XX:+DisableExplicitGC,禁止顯示手動GC
- -XX:+HeapDumpOnOutOfMemoryError,OOM時匯出堆快照便于分析問題
- -XX:+PrintGC,列印GC日志便于出問題時分析問題
-Xms4G
-Xmx4G
-Xmn3G
-Xss1M
-XX:SurvivorRatio=8
-XX:MetaspaceSize=512M
-XX:MaxMetaspaceSize=512M
-XX:MaxTenuringThreshold=5
-XX:PretenureSizeThreshold=1M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=92
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSWaitDuration=2000
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSParallelInitialMarkEnabled
-XX:+CMSScavengeBeforeRemark
-XX:+DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=dump.hprof
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:./gc.log
八、JVM引數
前面已經提到過很多JVM的引數了,這節再簡單匯總下,以及部分不常用的引數,
Java啟動引數共分為三類:
- 標準引數(-):所有的JVM實作都必須實作這些引數的功能,而且向后兼容,如 -version、-classpath
- 非標準引數(-X):默認jvm實作這些引數的功能,但是并不保證所有jvm實作都滿足,且不保證向后兼容,如 -Xms、-Xmx
- 非Stable引數(-XX):此類引數各個jvm實作會有所不同,將來可能會隨時取消,需要慎重使用,如 -XX:UseParNewGC、-XX:MetaspaceSize
1、JVM標準引數(-)
通過 java -help 命令可以看到JVM的標準引數

2、JVM非標準引數(-X)
通過 java -X 命令可以看到JVM非標準引數

常用引數:

3、JVM非Stable引數(-XX)
JVM非Stable引數分為三類:
- 功能開關引數:一些功能的開關,用于改變jvm的一些基礎行為
- 性能調優引數:用于jvm的性能調優
- 除錯引數:一般用于打開跟蹤、列印、輸出等jvm引數,用于顯示jvm更加詳細的資訊
注意:帶有加號“+”、減號“-”的引數一般為開關引數,加號就是啟用,減號就是禁用,如 -XX:+/-UseAdaptiveSizePolicy,不帶加減號的就需要通過等號“=”帶上引數值,如 -XX:SurvivorRatio=8,
可以通過設定 -XX:+PrintFlagsFinal 在啟動時列印所有JVM的引數及其值,
1)功能開關引數
① 垃圾回收器相關引數

② 其它的一些引數

2)性能調優引數

3)除錯引數

4、即時編譯調優引數
類初始化完成后,類在呼叫執行程序中,執行引擎會把位元組碼轉為機器碼,然后在作業系統中才能執行,在位元組碼轉換為機器碼的程序中,虛擬機中還存在著一道編譯,那就是即時編譯,最初,虛擬機中的位元組碼是由解釋器( Interpreter )完成編譯的,當虛擬機發現某個方法或代碼塊的運行特別頻繁的時候,就會把這些代碼認定為“熱點代碼”,為了提高熱點代碼的執行效率,在運行時,即時編譯器(JIT)會把這些代碼編譯成與本地平臺相關的機器碼,并進行各層次的優化,然后保存到記憶體中,如果沒有 JIT 即時編譯,每次運行相同的代碼都會使用解釋器編譯,
與編譯優化有關的主要有即時編譯器的選擇、熱點探測計數閥值的優化、方法行內、逃逸分析、鎖消除、標量替換等,一般來說也不用對編譯進行調優,這里就不展開說了,下面先列舉下編譯優化相關的一些JVM引數,

參考
本文是學習、參考了如下課程,再通過自己的總結和實踐總結而來,如果想了解更多深入的細節,建議閱讀原著,
《從 0 開始帶你成為JVM實戰高手》
《極客時間:Java性能調優實戰》
《深入理解Java虛擬機:JVM高級特性與最佳實踐 第三版》
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/169640.html
標籤:其他
上一篇:學習第49天
下一篇:實作Excel檔案的上傳和決議
