生產服務記憶體高問題
問題描述
-
1、“計算中心” 服務堆記憶體分配4g,在生產環境運行一段時間后,實際占用記憶體4.8G,業務運行正常,未出現OOM,(本文以此服務進行排查)
-
2、生產環境的老專案,均出現運行一段時間后,記憶體被占滿但未OOM的情況,部分實體因記憶體占用過高導致被系統kill,一般需要通過增加機器、實體進行解決(資源浪費),
造成的影響
-
1、服務器物理記憶體15g,部署了三個服務,如實際占用記憶體都超過4.8g,導致服務器物理記憶體不夠用,出現告警而將占用記憶體最大行程kill掉,影響生產服務的可用性,后果十分嚴重,
-
2、如服務申請的記憶體超出了JVM能提供的記憶體大小(記憶體泄漏),將會導致java堆記憶體溢位,從而發生full gc,導致服務回應大幅度變慢,卡機等狀態,
-
3、在公司大促等場景的情況下,記憶體占用很高的服務會帶來很大風險,通常需提前聯系運維同事對“計算中心”進行重啟,增加了開發及運維同事維護的作業量,
排查程序
代碼
(1)根據cat監控,獲取“計算中心”中的熱點方法,進行REVIEW,修正了部分可能會導致記憶體泄露的方法,并進行了觀察,
(2)通過VisualVM監控,定位到部分耗時較久的操作DB熱點方法,通過增加索引等方式,把查詢性能控制在毫秒級,
(3)dump“計算中心”的記憶體鏡像,通過MAT等工具觀察各個物件在堆空間中所占用的記憶體大小、類實體數量、物件參考關系,
結論:通過以上三點,未解決和定位“計算中心”記憶體高問題,由此可以認為,“計算中心”的記憶體占用問題與代碼無關,
系統
通過java ps| aux java 查看,“計算中心”服務實際占用的記憶體在4.9G左右,超過了JVM堆記憶體設定的大小但并未出現OOM,業務正常運行,通過free -g命令,可以發現buff/cache,3個g左右,centos中記憶體的分配是buff/cache + free + used=物理記憶體大小,系統分配給臨時檔案系統的大小默認是用掉一半的物理記憶體,這樣會造成buff/cache很大,而free很小,最終結論可能為服務記憶體沒有釋放使用了buff/cache,導致服務記憶體占用很高,

結論:和SRE溝通實際重啟服務后,記憶體使用率立刻降低,但是buff/cache的大小沒有變化,由此可以認為,“計算中心”的記憶體占用問題與系統快取無關,
JVM


1、通過VisualVM監控生產環境computing記憶體使用情況得出,在服務記憶體占用4.8g的情況下,堆記憶體(新生代 + 老年代)正常GC,在增長到2g左右會GC到300~500m,Matespace僅使用了120m左右,檢查了生產JVM引數,專案啟動引數沒有配置:-XX:MaxDirectMemorySize,來指定最大的堆外記憶體大小,這個閾值不配置的話,默認占用-Xmx相同的記憶體,在堆內記憶體正常的情況下,懷疑是堆外記憶體占用了大部分記憶體,導致服務記憶體占用很高,
結論:和SRE溝通,在生產環境找了兩臺計算中心服務實體配置 -XX:MaxDirectMemorySize 引數后實際觀察后,仍未解決“計算中心”記憶體高問題,由此可以認為,“計算中心”的記憶體占用問題與堆外記憶體無關,
2、在發現-XX:MaxDirectMemorySize 指定堆外記憶體大小的引數沒有配置后,我檢查了“計算中心”服務的啟動引數并且和之前生產環境的服務進行了對比,發現了以下問題,
1)生產及測驗環境JVM引數配置混亂,同一應用不同實體多套啟動引數配置,
2)服務啟動引數,未區分JDK版本,如:JDK1.7、JDK1.8引數混用,
3)生產服務根據模板部署,導致必要JVM引數未配置,部分引數配置不需要、不合理,
4)不同型別的應用,采用統一的啟動引數配置,不具有針對性,
在以上問題的基礎上,基于目前生產環境各專案統一使用的"CMS垃圾回收器"進行引數調整,針對計算中心的JDK版本(1.8),出了一套JVM配置方案,并在生產服務器調整后重啟觀察,
結論:“計算中心”生產環境服務,在運行一段時間后,仍出現記憶體占用高問題,由此可以認為,“計算中心”的記憶體占用問題與不同JDK版本的引數混用問題無關,
3、經調研,逐漸被淘汰的垃圾回收器比如ParallelOldGC和CMS,只要JVM申請過的記憶體,即使發生了GC回收了很多記憶體空間,JVM也不會把這些記憶體歸還給作業系統,這就會導致top命令中看到的RSS(行程RAM中實際保存的總記憶體)只會越來越高,而且一般都會超過Xmx的值,JDK1.9以后,默認的垃圾回收器已經選擇了G1,
G1相比CMS有更清晰的優勢:
1)CMS采用"標記-清理"演算法,所以它不能壓縮,最終導致記憶體碎片化問題,而G1采用了復制演算法,它通過把物件從若干個Region(獨立區域)拷貝到新的Region(獨立區域)程序中,執行了壓縮處理,垃圾回收后會整合空間,無記憶體碎片,
2)在G1中,堆是由Region(獨立區域)組成的,因此碎片化問題比CMS肯定要少的多,而且,當碎片化出現的時候,它只影響特定的Region(獨立區域),而不是影響整個堆中的老年代,
3)而且CMS必須掃描整個堆來確認存活物件,所以,長時間停頓是非常常見的,無法預測停頓時間,而G1的停頓時間取決于收集的Region(獨立區域)集合數量,在指定時間內只回收部分價值最大的空間,而不是整個堆的大小,所以相比起CMS,長時間停頓要少很多,可控很多,
4)G1選回收階段不會產生“浮動垃圾”,由于只回收部分Region(獨立區域),所以STW(stop-The-World機制簡稱STW,是在執行垃圾收集演算法時,Java應用程式的其他所有執行緒都被掛起)時間我們可控,所以不需要與用戶執行緒并發爭搶CPU資源,而CMS并發清理需要占據一部分的CPU,會降低吞吐量,G1由于STW,所以不會產生"浮動垃圾",CMS在并發清理階段會產生的無法回收的垃圾,
因此在以下場景下G1更適合:
1)服務端多核CPU、JVM記憶體占用較大的應用,
2)應用在運行程序中會產生大量記憶體碎片、需要經常壓縮空間,
3)想要更可控、可預期的GC停頓周期;防止高并發下應用雪崩現象,
結論:將”計算中心“使用的垃圾回識訓制升級為G1,并增加G1相關的優化記憶體的引數,在生產服務器進行觀察一周后發現服務記憶體始終穩定在了3.3G左右,業務處理性能穩定,成功解決了“計算中心”服務占用記憶體較高的問題,提升了系統的可用性,無需通過增加物理資源來提升服務整體性能,
CMS升級為G1方式
計算中心“生產全部服務實體部署的服務器,使用的是JDK1.8,JDK1.8支持G1垃圾回收器,故將服務啟動引數進行統一調整:
1)原引數(主要問題:使用CMS版本,JDK1.7,1.8引數混用,未指定堆外記憶體大小)
/opt/java/jdk1.8.0_102/bin/java -Dapp.home=${APP_HOME} -Dspring.profiles.active=prd -Dserver.port=${SERVER_PORT} -server -Xms4G -Xmx4G -Xmn2g -Xss256k -XX:PermSize=128m -XX:MaxPermSize=512m -Djava.awt.headless=true -Dfile.encoding=utf-8 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:AutoBoxCacheMax=20000 -XX:-OmitStackTraceInFastThrow -XX:ErrorFile=${APP_HOME}/logs/hs_err_%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${APP_HOME}/logs/ -Xloggc:${APP_HOME}/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -jar ${APP_HOME}/webapps/${JAR_NAME} ${SERVER_PORT}"
2)新引數(使用G1做為垃圾回收器)
/opt/java/jdk1.8.0_102/bin/java -Dapp.home=${APP_HOME} -Dspring.profiles.active=prd -Dserver.port=${SERVER_PORT} -server -Xms4g -Xmx4g -Xss256k -XX:NewSize=512m -XX:MaxNewSize=512m -XX:+UseG1GC -XX:InitiatingHeapOccupancyPercent=40 -XX:G1HeapRegionSize=8m -XX:+ExplicitGCInvokesConcurrent -XX:ParallelGCThreads=4 -Dsun.rmi.dgc.server.gcInterval=36000000-Dsun.rmi.dgc.client.gcInterval=36000000-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:+UseCodeCacheFlushing -XX:ReservedCodeCacheSize=256m -XX:MaxDirectMemorySize=512m -XX:GCTimeRatio=19 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=30 -XX:ErrorFile=${APP_HOME}/logs/hs_err_%p.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${APP_HOME}/logs/ -Xloggc:${APP_HOME}/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -jar ${APP_HOME}/webapps/${JAR_NAME} ${SERVER_PORT}"
注:不同服務器環境,不同容器,腳本配置各不相同,要在對應腳本的基礎上進行針對性升級,
生產G1回收器主要引數說明

JVM相關概念說明
JDK1.7記憶體模型
實際占用記憶體大小(引數):-XX:MaxPermSize(非堆) + -Xmx(堆) + -Xss(堆疊) + -XX:MaxDirectMemorySize(堆外)

JDK1.8記憶體模型
實際占用記憶體大小(引數):-XX:MaxMateSpaceSize(堆外) + -Xmx(堆) + -Xss(堆疊) + -XX:MaxDirectMemorySize(堆外)

GC流程圖
1、什么時候觸發Minor GC
2、觸發Minor GC 的程序
3、Full GC 的程序

1、新創建的物件一般會被分配在新生代中,常用的新生代的垃圾回收器是 ParNew 垃圾回收器,它按照 8:1:1 將新生代分成 Eden 區,以及兩個 Survivor 區,創建的物件將 Eden 區全部擠滿,這個物件就是「擠滿新生代的最后一個物件」,此時,Minor GC 就觸發了,
2、在正式 Minor GC 前,JVM 會先檢查新生代中物件,是比老年代中剩余空間大還是小,Minor GC 之后 Survivor 區放不下剩余物件,這些物件就要進入到老年代,所以要提前檢查老年代是不是夠用,
3、老年代剩余空間大于新生代中的物件大小,那就直接 Minor GC,GC 完 survivor 不夠放,老年代也絕對夠放,老年代剩余空間小于新生代中的物件大小,這時候就要進入老年代空間分配擔保規則,
4、老年代空間分配擔保規則:如果老年代中剩余空間大小,大于歷次 Minor GC 之后剩余物件的大小,那就允許進行 Minor GC,因為從概率上來說,以前的放的下,這次的也應該放的下,那就有兩種情況:
-
老年代中剩余空間大小,大于歷次 Minor GC 之后剩余物件的大小,進行 Minor GC
-
老年代中剩余空間大小,小于歷次 Minor GC 之后剩余物件的大小,進行 Full GC,把老年代空出來再檢查,
-
結合第四步,開啟老年代空間分配擔保規則只能說是大概率上來說,Minor GC 剩余后的物件夠放到老年代,如果放不下:Minor GC 后會有這樣三種情況:
-
Minor GC 之后的物件足夠放到 Survivor 區,GC 結束,
-
Minor GC 之后的物件不夠放到 Survivor 區,接著進入到老年代,老年代能放下,那也可以,GC 結束,
-
Minor GC 之后的物件不夠放到 Survivor 區,老年代也放不下,那就只能 Full GC,
-
以上是成功 GC 的例子,以下3 中情況,會導致 GC 失敗,報 OOM:
緊接上一節 Full GC 之后,老年代任然放不下剩余物件,就只能 OOM,
未開啟老年代分配擔保機制,且一次 Full GC 后,老年代任然放不下剩余物件,也只能 OOM,
開啟老年代分配擔保機制,但是擔保不通過,一次 Full GC 后,老年代任然放不下剩余物件,也是能 OOM,
注:
- 老年代分配擔保機制在JDK1.5以及之前版本中默認是關閉的,需要通過HandlePromotionFailure手動指定,JDK1.6之后就默認開啟,如果我們生產環境服務使用的是JDK/1.7JDK1.8,所以不用再手動去開啟擔保機制,
- Full GC主要指新生代、老年代、metaspace上的全部GC,
感謝以下作者給與我的幫助
- 圖解GC流程:https://www.cnblogs.com/shuiyj/p/12640692.html
- CMS垃圾回收升級G1回收器實踐:http://arick.net/content/44
- JAVA常見問題分析:https://blog.51cto.com/hmtk520/2067043
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/149321.html
標籤:Java
