JAVA服務實例內存高問題排查及解決


生產服務內存高問題

問題描述

  • 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。

感謝以下作者給與我的幫助


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM