JVM之調優及常見場景分析


JVM調優

微信圖片_20201127154300

GC調優是最后要做的工作,GC調優的目的可以總結為下面兩點:

  • 減少對象晉升到老年代的數量
  • 減少FullGC的執行時間

通過監控排查問題及驗證優化結果,可以分為:

如果GC執行時間滿足下列所有條件,就沒有必要進行GC優化了:

  • Minor GC執行非常迅速(50ms以內)
  • Minor GC沒有頻繁執行(大約10s執行一次)
  • Full GC執行非常迅速(1s以內)
  • Full GC沒有頻繁執行(大約10min執行一次)

案例參考:

常見場景分析

動態擴容引起的空間震盪

現象

服務剛剛啟動時 GC 次數較多,最大空間剩余很多但是依然發生 GC,這種情況我們可以通過觀察 GC 日志或者通過監控工具來觀察堆的空間變化情況即可。GC Cause 一般為 Allocation Failure,且在 GC 日志中會觀察到經歷一次 GC ,堆內各個空間的大小會被調整,如下圖所示:

圖片

原因分析

在 JVM 的參數中 -Xms-Xmx 設置的不一致,在初始化時只會初始 -Xms 大小的空間存儲信息,每當空間不夠用時再向操作系統申請,這樣的話必然要進行一次 GC。另外,如果空間剩余很多時也會進行縮容操作,JVM 通過 -XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio 來控制擴容和縮容的比例,調節這兩個值也可以控制伸縮的時機。

解決方案

盡量將成對出現的空間大小配置參數設置成固定的,如 -Xms-Xmx-XX:MaxNewSize-XX:NewSize-XX:MetaSpaceSize-XX:MaxMetaSpaceSize 等。不過在不追求停頓時間的情況下震盪的空間也是有利的,可以動態地伸縮以節省空間,例如作為富客戶端的 Java 應用。

顯式GC的去和留

現象

手動調用 System.gc 方法會引發一次 STW 的 Full GC,對整個堆做收集,可以在 GC 日志中的 GC Cause 中確認。同時JVM提供-XX:+DisableExplicitGC 參數可以避免這種 GC。那么有沒有必要啟用該參數呢?

去留分析

首先需要了解下DirectByteBuffer,它有着零拷貝等特點,被 Netty 等各種 NIO 框架使用,會使用到堆外內存。它的 Native Memory 的清理工作是通過 sun.misc.Cleaner 自動完成的,是一種基於虛引用PhantomReference的清理工具,比普通的 Finalizer 輕量些。而為 DirectByteBuffer 分配空間過程中會顯式調用 System.gc ,希望通過 Full GC 來強迫已經無用的 DirectByteBuffer 對象釋放掉它們關聯的 Native Memory。

如果通過-XX:+DisableExplicitGC關閉顯式GC,DirectByteBuffer分配空間中System.gc將失效,這時如果很長一段時間沒有做過GC或者只做了Young GC,則不會觸發Cleaner 的工作,Native Memory得不到及時釋放,有可能發生內存泄漏。

所以一般建議保留顯式GC,但需要規范使用,避免頻繁GC帶來的性能開銷。可通過-XX:+ExplicitGCInvokesConcurrent-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 參數來將 System.gc 的觸發類型從 Foreground 改為 Background,同時 Background 也會做 Reference Processing,這樣的話就能大幅降低了 STW 開銷,同時也不會發生 NIO Direct Memory OOM。

MetaSpace 區 OOM

現象

JVM 在啟動后或者某個時間點開始,MetaSpace 的已使用大小在持續增長,同時每次 GC 也無法釋放,調大 MetaSpace 空間也無法徹底解決

原因分析

Java 7 之前字符串常量池被放到了 Perm 區,所有被 intern 的 String 都會被存在這里,由於 String.intern 是不受控的,所以 -XX:MaxPermSize 的值也不太好設置,經常會出現 java.lang.OutOfMemoryError: PermGen space 異常。但在 Java 7 之后常量池等字面量(Literal)、類靜態變量(Class Static)、符號引用(Symbols Reference)等幾項被移到 Heap 中,PermGen 也被移除,取而代之的是 MetaSpace。在最底層,JVM 通過 mmap 接口向操作系統申請內存映射,每次申請 2MB 空間,這里是虛擬內存映射,不是真的就消耗了主存的 2MB,只有之后在使用的時候才會真的消耗內存。申請的這些內存放到一個鏈表中 VirtualSpaceList,作為其中的一個 Node。

關鍵原因就是 ClassLoader 不停地在內存中 load 了新的 Class ,一般這種問題都發生在動態類加載等情況上。

解決方案

dump 快照之后通過 JProfiler 或 MAT 觀察 Classes 的 Histogram(直方圖)即可,或者直接通過命令即可定位, jcmd 打幾次 Histogram 的圖,看一下具體是哪個包下的 Class 增加較多就可以定位了。

jcmd <PID> GC.class_stats|awk '{print$13}'|sed  's/\(.*\)\.\(.*\)/\1/g'|sort |uniq -c|sort -nrk1

經常會出問題的幾個點有 Orika 的 classMap、JSON 的 ASMSerializer、Groovy 動態加載類等,基本都集中在反射、Javasisit 字節碼增強、CGLIB 動態代理、OSGi 自定義類加載器等的技術點上。

過早晉升

現象

  • 分配速率接近於晉升速率,對象晉升年齡較小
  • Full GC 比較頻繁,且經歷過一次 GC 之后 Old 區的變化比例非常大

原因分析及策略

  • Young/Eden 區過小:一般情況下 Old 的大小應當為活躍對象的 2~3 倍左右,考慮到浮動垃圾問題最好在 3 倍左右,剩下的都可以分給 Young 區
  • 分配速率過大
    • 偶發較大:通過內存分析工具找到問題代碼,從業務邏輯上做一些優化
    • 一直較大:當前的 Collector 已經不滿足應用程序的期望了,這種情況要么增加應用程序的 機器,要么調整 GC 收集器類型或加大空間

CMS Old GC頻繁

現象

Old 區頻繁的做 CMS GC,但是每次耗時不是特別長,整體最大 STW 也在可接受范圍內,但由於 GC 太頻繁導致吞吐下降比較多。

原因分析

基本都是一次 Young GC 完成后,負責處理 CMS GC 的一個后台線程 concurrentMarkSweepThread 會不斷地輪詢,使用 shouldConcurrentCollect() 方法做一次檢測,判斷是否達到了回收條件。如果達到條件(參考上文中CMS GC觸發條件),使用 collect_in_background() 啟動一次 Background 模式 GC。輪詢的判斷是使用 sleepBeforeNextCycle() 方法,間隔周期為 -XX:CMSWaitDuration 決定,默認為2s。

解決方案

圖片

  • Dump Diff:分別在 CMS GC 的發生前后分別 dump 一次,進行dump文件差異分析
  • Leak Suspects:內存泄露報告
  • Top Component分析:按照對象、類、類加載器、包等多個維度觀察 Histogram,同時使用 outgoing 和 incoming 分析關聯的對象,另外就是 Soft Reference 和 Weak Reference、Finalizer 等也要看一下
  • Unreachable分析:不可達對象分析

單次 CMS Old GC 耗時長

現象

CMS GC 單次 STW 最大超過 1000ms,不會頻繁發生。但這種場景非常危險,某些場景下會引起“雪崩效應”,我們應該盡量避免出現。

原因分析

可能造成STW的情況如下:

  • Init Mark

    圖片

    整個過程比較簡單,從 GC Root 出發標記 Old 中的對象,處理完成后借助 BitMap 處理下 Young 區對 Old 區的引用,整個過程基本都比較快,很少會有較大的停頓。

  • Final Mark

    Final Remark 的開始階段與 Init Mark 處理的流程相同,但是后續多了 Card Table 遍歷、Reference 實例的清理,並將其加入到 Reference 維護的 pend_list 中,如果要收集元數據信息,還要清理 SystemDictionary、CodeCache、SymbolTable、StringTable 等組件中不再使用的資源。

  • STW前等待應用線程到達安全點(較少發生)

由此可見,大部分問題都出在 Final Remark 過程,觀察詳細 GC 日志,找到出問題時 Final Remark 日志,分析下 Reference 處理和元數據處理 real 耗時是否正常,詳細信息需要通過 -XX:+PrintReferenceGC 參數開啟。基本在日志里面就能定位到大概是哪個方向出了問題,耗時超過 10% 的就需要關注

一般來說最容易出問題的地方就是 Reference 中的 FinalReference 和元數據信息處理中的 scrub symbol table 兩個階段,想要找到具體問題代碼就需要內存分析工具 MAT 或 JProfiler 了,注意要 dump 即將開始 CMS GC 的堆。在用 MAT 等工具前也可以先用命令行看下對象 Histogram,有可能直接就能定位問題。

  • 對 FinalReference 的分析主要觀察 java.lang.ref.Finalizer 對象的 dominator tree,找到泄漏的來源。經常會出現問題的幾個點有 Socket 的 SocksSocketImpl 、Jersey 的 ClientRuntime、MySQL 的 ConnectionImpl 等等。
  • scrub symbol table 表示清理元數據符號引用耗時,符號引用是 Java 代碼被編譯成字節碼時,方法在 JVM 中的表現形式,生命周期一般與 Class 一致,當 _should_unload_classes 被設置為 true 時在 CMSCollector::refProcessingWork() 中與 Class Unload、String Table 一起被處理。

解決方案

一般不會大面積同時爆發,不過有很多時候單台 STW 的時間會比較長,如果業務影響比較大,及時摘掉流量,具體后續優化策略如下:

  • FinalReference:找到內存來源后通過優化代碼的方式來解決,如果短時間無法定位可以增加 -XX:+ParallelRefProcEnabled 對 Reference 進行並行處理。
  • symbol table:觀察 MetaSpace 區的歷史使用峰值,以及每次 GC 前后的回收情況,一般沒有使用動態類加載或者 DSL 處理等,MetaSpace 的使用率上不會有什么變化,這種情況可以通過 -XX:-CMSClassUnloadingEnabled 來避免 MetaSpace 的處理,JDK8 會默認開啟 CMSClassUnloadingEnabled,這會使得 CMS 在 CMS-Remark 階段嘗試進行類的卸載。

內存碎片&收集器退化

現象

並發的 CMS GC 算法,退化為 Foreground 單線程串行 GC 模式,STW 時間超長,有時會長達十幾秒。其中 CMS 收集器退化后單線程串行 GC 算法有兩種:

  • 帶壓縮動作的算法,稱為 MSC,上面我們介紹過,使用標記-清理-壓縮,單線程全暫停的方式,對整個堆進行垃圾收集,也就是真正意義上的 Full GC,暫停時間要長於普通 CMS。
  • 不帶壓縮動作的算法,收集 Old 區,和普通的 CMS 算法比較相似,暫停時間相對 MSC 算法短一些。

原型分析

  • 晉升失敗(Promotion Failed):old空間不足或者碎片導致晉升失敗,由於concurrentMarkSweepThread 和擔保機制的存在,發生的條件是很苛刻的
  • 增量收集擔保失敗:分配內存失敗后,會判斷統計得到的 Young GC 晉升到 Old 的平均大小,以及當前 Young 區已使用的大小也就是最大可能晉升的對象大小,是否大於 Old 區的剩余空間。只要 CMS 的剩余空間比前兩者的任意一者大,CMS 就認為晉升還是安全的,反之,則代表不安全,不進行Young GC,直接觸發Full GC。
  • 顯示GC
  • 並發模式失敗(Concurrent Mode Failure)

解決方案

分析到具體原因后,我們就可以針對性解決了,具體思路還是從根因出發,具體解決策略:

  • 內存碎片:通過配置 -XX:UseCMSCompactAtFullCollection=true 來控制 Full GC的過程中是否進行空間的整理(默認開啟,注意是Full GC,不是普通CMS GC),以及 -XX: CMSFullGCsBeforeCompaction=n 來控制多少次 Full GC 后進行一次壓縮(可以使用 -XX:PrintFLSStatistics 來觀察內存碎片率情況,然后再設置具體的值)
  • 增量收集:降低觸發 CMS GC 的閾值,即參數 -XX:CMSInitiatingOccupancyFraction 的值,讓 CMS GC 盡早執行,以保證有足夠的連續空間,也減少 Old 區空間的使用大小,另外需要使用 -XX:+UseCMSInitiatingOccupancyOnly 來配合使用,不然 JVM 僅在第一次使用設定值,后續則自動調整。
  • 浮動垃圾:視情況控制每次晉升對象的大小,或者縮短每次 CMS GC 的時間,必要時可調節 NewRatio 的值。另外就是使用 -XX:+CMSScavengeBeforeRemark 在過程中提前觸發一次 Young GC,防止后續晉升過多對象。

堆外內存OOM

現象

內存使用率不斷上升,甚至開始使用 SWAP 內存,同時可能出現 GC 時間飆升,線程被 Block 等現象,通過 top 命令發現 Java 進程的 RES 甚至超過了 -Xmx 的大小。出現這些現象時,基本可以確定是出現了堆外內存泄漏。

原因分析

JVM 的堆外內存泄漏,主要有兩種的原因:

  • 通過 UnSafe#allocateMemoryByteBuffer#allocateDirect 主動申請了堆外內存而沒有釋放,常見於 NIO、Netty 等相關組件。
  • 代碼中有通過 JNI 調用 Native Code 申請的內存沒有釋放。

解決方案

首先可以使用 NMT(NativeMemoryTracking) + jcmd 分析泄漏的堆外內存是哪里申請,確定原因后,使用不同的手段,進行原因定位。

圖片


免責聲明!

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



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