博客 / 詳情

返回

揭開 Java 容器“消失的內存”之謎:雲監控 2.0 SysOM 診斷實踐

背景

在前一篇文章《一次內存診斷,讓資源利用率提升 40%:揭秘隱式內存治理》[1]中,我們系統性地剖析了雲原生環境中隱性內存開銷的診斷方法,通過 SysOM 系統診斷實現了對節點/Pod 級由文件緩存、共享內存等系統級內存資源異常消耗的精準定位。

然而,部分場景下內存異常仍可能源於應用進程本身的內存申請,但是對於應用內存泄漏問題,儘管是應用的開發者,也需要投入大量的精力去利用對應語言的內存分析工具去找出根因;以 Java 應用為例,當傳統線下 IDC 集羣中的 Java 應用完成雲原生架構轉型後,伴隨容器化封裝與資源配額管控的實施,用户普遍反饋 Java 應用 Pod 出現持續性內存超限及 Kubernetes OOMKilled 事件。這一系列現象主要集中在三個關鍵矛盾點:

  1. 容器內存監控與 JVM 堆內存的顯著差異:Pod 內存佔用常超出 JVM 堆內存(含堆外內存)數倍,形成“消失的內存”謎團。
  2. 容器化改造後的 OS 兼容性問題:同一業務系統在切換 OS 或容器化後,出現內存佔用模式突變。
  3. 工具鏈覆蓋盲區:傳統 Java 內存分析工具無法覆蓋 JNI 內存、LIBC 內存等非 JVM 內存區域。

為此,雲監控 2.0[2]中的 SysOM 系統診斷對應用內存進一步深挖,結合應用和操作系統的角度實現對主機、容器運行時及具體的 Java 應用進程進行內存佔用拆解,快速有效地識別出 Java 內存佔用的元兇。

Java 內存全景分析

為了找出消失的內存,我們首先要了解 Java 進程的主要內存組成以及現有工具和監控主要覆蓋的部分;如下圖所示可分為:

JVM 內存

堆內存:可通過 -Xms/-Xmx 參數控制,內存大小可通過 memorymxbean 等獲取。
堆外內存:包括元空間、壓縮類空間、代碼緩衝區、直接緩衝、線程棧等內存組成;它們分別可以通過 -XX:MaxMetaspaceSize(元空間)、-XX:CompressedClassSpaceSize(壓縮類空間)、-XX:ReservedCodeCacheSize(代碼緩衝區)、-XX:MaxDirectMemorySize (直接緩衝)、-Xss(線程棧)參數限制。

非 JVM 內存

JNI 本地內存:即通過本地方法接口調用 C、C++ 代碼(原生庫),並在這部分代碼中調用 C 庫(malloc)或系統調用(brk、mmap)直接分配的內存。

圖片

Java 常見“內存泄露”

JNI 內存泄漏

經過上一章中對 Java 內內存全景的分析,其實已經可以揭開第一個容易造成內存黑洞的隱藏 Boss-JNI 內存,因為這部分內存暫時沒有工具可以獲取其佔用大小。

通常來説,編寫相關業務代碼的同學會認為代碼中沒有使用本地方法直接調用 C 庫,所以不會存在這些問題,但是代碼中引用的各種包卻有可能會使用到 JNI 內存,比如説經典的使用 ZLIB 壓縮庫不當導致的 JNI 泄漏問題[3]。

LIBC 內存管理特性

JVM 向 OS 申請內存的中間,還存在着一層中間層 -C 庫,JVM 調用 malloc、free 申請/釋放內存的過程中其實還要經過這一個二道販子;以 gibc 中默認的內存分配器 ptmalloc 為例 glibc 的 ptmalloc 內存分配器存在以下特徵:

  • Arena 機制:每個線程維護 64M Arena,多線程場景下易產生內存碎片
  • Top Chunk 管理:內存空洞導致無法及時歸還 OS
  • Bins 緩存策略:JVM 釋放的內存暫存於 bins 中,造成統計偏差 [4-5]

圖片

Linux 透明大頁(THP)影響

在 OS 層,Linux 中的透明大頁(Transparent Huge Page)機制也是造成 JVM 內存和實際內存差異的一大元兇。簡單來説,THP 機制就是 OS 會將 4kb 頁變成 2M 的大頁,從而減少 TLB miss 和缺頁中斷,提升應用性能,但是也帶來了一些內存浪費。如應用申請了一段 2M 的虛擬內存,但實際只用了裏面的 4kb,但是由於 THP 機制,OS 已經分配了一個 2M 的頁了[6]。

SysOM Java 內存診斷實踐

下面將以汽車行業客户在從線下 idc 集羣遷移至雲上 ACK 集羣時遇到的由於 JNI 內存泄漏導致 Pod 頻繁 OOM 為例,介紹如何通過雲監控 2.0 的 SysOM 系統診斷來一步步找出 Java 內存佔用的元兇。

診斷使用限制:

  • 目前僅支持 openJDK 1.8 以上版本
  • 使用 JNI 內存 Profiling 功能需要至操作系統控制枱先對實例進行納管[3],有一定的資源和性能開銷(內存佔用根據符號大小最高達 300MB)

C2 compiler JIT 內存膨脹案例

案例背景
某汽車客户在 ACK 集羣遷移過程中,多個 Java 服務 Pod 出現偶發性 OOM。特徵表現為:

  • Pod 內存接近限制時觸發 OOM
  • JVM 監控顯示內存正常
  • 無明顯請求異常或流量波動

排查過程

嘗試在內存高水位時對 Pod 發起內存全景分析。
圖片

  • 我們可以瞭解到當 Pod 中容器內存使用已經接近 limit,從診斷結論和容器內存佔用分析中,我們可以看到容器內存主要是由於 Java 進程內存佔用導致。

圖片

對 Java 進程發起內存分析,查看診斷報告。報告展示了 Java 進程所在 Pod 和容器的 rss 和 WorkingSet(工作集)內存信息、進程 Pid、JVM 內存使用量(即 JVM 視角的內存使用量)、Java 進程內存使用量(進程實際佔用內存),進程匿名用量以及進程文件內存用量。

圖片

通過診斷結論和 Java 內存佔用餅圖我們可以發現,進程實際內存佔用比 JVM 監控顯示的內存佔用大 570M,全都由 JNI 內存所貢獻[4]。

圖片

開啓 JNI(Java Native Interface)內存分配 profiling,報告列出當前 Java 進程 JNI 內存分配調用火焰圖,火焰圖中為所有分配 JNI 內存的調用路徑。(説明:由於是採樣採集,火焰圖中的內存大小不代表實際分配大小)。

圖片

  • 從內存分配火焰圖中,我們可以看到主要的內存申請為 C2 compiler 正在進行代碼 JIT 預熱;
  • 但是由於診斷的過程中沒有發現 pod 有內存突增;所以我們進一步藉助可以常態化運行的 Java CPU 熱點追蹤功能[5]嘗試抓取內存升高時的進程熱點,並通過熱點對比[6]嘗試對內存正常時的熱點進行對比。

圖片

圖片

  • 通過熱點棧和熱點分析對比,發現內存突增時間點的 cpu 棧也是 c2 compiler 的 JIT 棧,且 c2 compiler 熱點前有部分業務流量突增,且業務代碼使用了大量反射操作(反射操作會導致 c2 compiler 進行新的預熱)。

結論和解決方案

C2 compiler JIT 過程申請 JNI 內存,且由於 glibc 內存空洞等原因導致申請內存放大且延時釋放。

  1. 調整 C2 compiler 參數,讓其編譯策略更保守,可以嘗試調整相關參數,觀察內存消耗變化。
  2. 調整 glibc 環境變量 MALLOC_TRIM_THRESHOLD_,讓 glibc 及時將內存釋放回操作系統。

總結

通過系統化的內存診斷方法,我們得以穿透 JVM 黑盒,揭示 JNI、LIBC 及 OS 層面的內存管理特性。阿里雲操作系統控制枱的內存全景分析功能,為容器化 Java 應用提供了從進程級到系統級的立體化診斷能力,幫助開發者精準定位內存異常根源,有效避免 OOM 事件的發生。

相關鏈接:

[1]《一次內存診斷,讓資源利用率提升 40%:揭秘隱式內存治理》

[2] 雲監控-ECS 洞察-SysOM 系統診斷
https://cmsnext.console.aliyun.com/next/region/cn-shanghai/wo...

[3] 操作系統控制枱實例納管
https://help.aliyun.com/zh/alinux/user-guide/system-managemen...

[4] 操作系統控制枱 Java 內存診斷
https://help.aliyun.com/zh/alinux/user-guide/java-memory-diag...

[5] 操作系統控制枱熱點追蹤
https://help.aliyun.com/zh/alinux/user-guide/process-hotspot-...

[6] 操作系統控制枱熱點對比分析
https://help.aliyun.com/zh/alinux/user-guide/hot-spot-compara...

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.