全網最硬核 JVM 記憶體解析 - 1.從 Native Memory Tracking 說起

乾貨滿滿張雜湊發表於2023-04-26

個人創作公約:本人宣告創作的所有文章皆為自己原創,如果有參考任何文章的地方,會標註出來,如果有疏漏,歡迎大家批判。如果大家發現網上有抄襲本文章的,歡迎舉報,並且積極向這個 github 倉庫 提交 issue,謝謝支援~
另外,本文為了避免抄襲,會在不影響閱讀的情況下,在文章的隨機位置放入對於抄襲和洗稿的人的“親切”的問候。如果是正常讀者看到,筆者在這裡說聲對不起,。如果被抄襲狗或者洗稿狗看到了,希望你能夠好好反思,不要再抄襲了,謝謝。
今天又是乾貨滿滿的一天,這是全網最硬核 JVM 解析系列第四篇,往期精彩:

本篇是關於 JVM 記憶體的詳細分析。網上有很多關於 JVM 記憶體結構的分析以及圖片,但是由於不是一手的資料亦或是人云亦云導致有很錯誤,造成了很多誤解;並且,這裡可能最容易混淆的是一邊是 JVM Specification 的定義,一邊是 Hotspot JVM 的實際實現,有時候人們一些部分說的是 JVM Specification,一部分說的是 Hotspot 實現,給人一種割裂感與誤解。本篇主要從 Hotspot 實現出發,以 Linux x86 環境為主,緊密貼合 JVM 原始碼並且輔以各種 JVM 工具驗證幫助大家理解 JVM 記憶體的結構。但是,本篇僅限於對於這些記憶體的用途,使用限制,相關引數的分析,有些地方可能比較深入,有些地方可能需要結合本身用這塊記憶體涉及的 JVM 模組去說,會放在另一系列文章詳細描述。最後,洗稿抄襲狗不得 house

本篇全篇目錄(以及涉及的 JVM 引數):

  1. 從 Native Memory Tracking 說起(全網最硬核 JVM 記憶體解析 - 1.從 Native Memory Tracking 說起開始)
    1. Native Memory Tracking 的開啟
    2. Native Memory Tracking 的使用(涉及 JVM 引數:NativeMemoryTracking
    3. Native Memory Tracking 的 summary 資訊每部分含義
    4. Native Memory Tracking 的 summary 資訊的持續監控
    5. 為何 Native Memory Tracking 中申請的記憶體分為 reserved 和 committed
  2. JVM 記憶體申請與使用流程(全網最硬核 JVM 記憶體解析 - 2.JVM 記憶體申請與使用流程開始)
    1. Linux 下記憶體管理模型簡述
    2. JVM commit 的記憶體與實際佔用記憶體的差異
      1. JVM commit 的記憶體與實際佔用記憶體的差異
    3. 大頁分配 UseLargePages(全網最硬核 JVM 記憶體解析 - 3.大頁分配 UseLargePages開始)
      1. Linux 大頁分配方式 - Huge Translation Lookaside Buffer Page (hugetlbfs)
      2. Linux 大頁分配方式 - Transparent Huge Pages (THP)
      3. JVM 大頁分配相關引數與機制(涉及 JVM 引數:UseLargePages,UseHugeTLBFS,UseSHM,UseTransparentHugePages,LargePageSizeInBytes
  3. Java 堆記憶體相關設計(全網最硬核 JVM 記憶體解析 - 4.Java 堆記憶體大小的確認開始)
    1. 通用初始化與擴充套件流程
    2. 直接指定三個指標的方式(涉及 JVM 引數:MaxHeapSize,MinHeapSize,InitialHeapSize,Xmx,Xms
    3. 不手動指定三個指標的情況下,這三個指標(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何計算的
    4. 壓縮物件指標相關機制(涉及 JVM 引數:UseCompressedOops)(全網最硬核 JVM 記憶體解析 - 5.壓縮物件指標相關機制開始)
      1. 壓縮物件指標存在的意義(涉及 JVM 引數:ObjectAlignmentInBytes
      2. 壓縮物件指標與壓縮類指標的關係演進(涉及 JVM 引數:UseCompressedOops,UseCompressedClassPointers
      3. 壓縮物件指標的不同模式與定址最佳化機制(涉及 JVM 引數:ObjectAlignmentInBytes,HeapBaseMinAddress
    5. 為何預留第 0 頁,壓縮物件指標 null 判斷擦除的實現(涉及 JVM 引數:HeapBaseMinAddress
    6. 結合壓縮物件指標與前面提到的堆記憶體限制的初始化的關係(涉及 JVM 引數:HeapBaseMinAddress,ObjectAlignmentInBytes,MinHeapSize,MaxHeapSize,InitialHeapSize
    7. 使用 jol + jhsdb + JVM 日誌檢視壓縮物件指標與 Java 堆驗證我們前面的結論
      1. 驗證 32-bit 壓縮指標模式
      2. 驗證 Zero based 壓縮指標模式
      3. 驗證 Non-zero disjoint 壓縮指標模式
      4. 驗證 Non-zero based 壓縮指標模式
    8. 堆大小的動態伸縮(涉及 JVM 引數:MinHeapFreeRatio,MaxHeapFreeRatio,MinHeapDeltaBytes)(全網最硬核 JVM 記憶體解析 - 6.其他 Java 堆記憶體相關的特殊機制開始)
    9. 適用於長期執行並且儘量將所有可用記憶體被堆使用的 JVM 引數 AggressiveHeap
    10. JVM 引數 AlwaysPreTouch 的作用
    11. JVM 引數 UseContainerSupport - JVM 如何感知到容器記憶體限制
    12. JVM 引數 SoftMaxHeapSize - 用於平滑遷移更耗記憶體的 GC 使用
  4. JVM 元空間設計(全網最硬核 JVM 記憶體解析 - 7.元空間儲存的後設資料開始)
    1. 什麼是後設資料,為什麼需要後設資料
    2. 什麼時候用到元空間,元空間儲存什麼
      1. 什麼時候用到元空間,以及釋放時機
      2. 元空間儲存什麼
    3. 元空間的核心概念與設計(全網最硬核 JVM 記憶體解析 - 8.元空間的核心概念與設計開始)
      1. 元空間的整體配置以及相關引數(涉及 JVM 引數:MetaspaceSize,MaxMetaspaceSize,MinMetaspaceExpansion,MaxMetaspaceExpansion,MaxMetaspaceFreeRatio,MinMetaspaceFreeRatio,UseCompressedClassPointers,CompressedClassSpaceSize,CompressedClassSpaceBaseAddress,MetaspaceReclaimPolicy
      2. 元空間上下文 MetaspaceContext
      3. 虛擬記憶體空間節點列表 VirtualSpaceList
      4. 虛擬記憶體空間節點 VirtualSpaceNodeCompressedClassSpaceSize
      5. MetaChunk
        1. ChunkHeaderPool 池化 MetaChunk 物件
        2. ChunkManager 管理空閒的 MetaChunk
      6. 類載入的入口 SystemDictionary 與保留所有 ClassLoaderDataClassLoaderDataGraph
      7. 每個類載入器私有的 ClassLoaderData 以及 ClassLoaderMetaspace
      8. 管理正在使用的 MetaChunkMetaspaceArena
      9. 元空間記憶體分配流程(全網最硬核 JVM 記憶體解析 - 9.元空間記憶體分配流程開始)
        1. 類載入器到 MetaSpaceArena 的流程
        2. MetaChunkArena 普通分配 - 整體流程
        3. MetaChunkArena 普通分配 - FreeBlocks 回收老的 current chunk 與用於後續分配的流程
        4. MetaChunkArena 普通分配 - 嘗試從 FreeBlocks 分配
        5. MetaChunkArena 普通分配 - 嘗試擴容 current chunk
        6. MetaChunkArena 普通分配 - 從 ChunkManager 分配新的 MetaChunk
        7. MetaChunkArena 普通分配 - 從 ChunkManager 分配新的 MetaChunk - 從 VirtualSpaceList 申請新的 RootMetaChunk
        8. MetaChunkArena 普通分配 - 從 ChunkManager 分配新的 MetaChunk - 將 RootMetaChunk 切割成為需要的 MetaChunk
        9. MetaChunk 回收 - 不同情況下, MetaChunk 如何放入 FreeChunkListVector
      10. ClassLoaderData 回收
    4. 元空間分配與回收流程舉例(全網最硬核 JVM 記憶體解析 - 10.元空間分配與回收流程舉例開始)
      1. 首先類載入器 1 需要分配 1023 位元組大小的記憶體,屬於類空間
      2. 然後類載入器 1 還需要分配 1023 位元組大小的記憶體,屬於類空間
      3. 然後類載入器 1 需要分配 264 KB 大小的記憶體,屬於類空間
      4. 然後類載入器 1 需要分配 2 MB 大小的記憶體,屬於類空間
      5. 然後類載入器 1 需要分配 128KB 大小的記憶體,屬於類空間
      6. 新來一個類載入器 2,需要分配 1023 Bytes 大小的記憶體,屬於類空間
      7. 然後類載入器 1 被 GC 回收掉
      8. 然後類載入器 2 需要分配 1 MB 大小的記憶體,屬於類空間
    5. 元空間大小限制與動態伸縮(全網最硬核 JVM 記憶體解析 - 11.元空間分配與回收流程舉例開始)
      1. CommitLimiter 的限制元空間可以 commit 的記憶體大小以及限制元空間佔用達到多少就開始嘗試 GC
      2. 每次 GC 之後,也會嘗試重新計算 _capacity_until_GC
    6. jcmd VM.metaspace 元空間說明、元空間相關 JVM 日誌以及元空間 JFR 事件詳解(全網最硬核 JVM 記憶體解析 - 12.元空間各種監控手段開始)
      1. jcmd <pid> VM.metaspace 元空間說明
      2. 元空間相關 JVM 日誌
      3. 元空間 JFR 事件詳解
        1. jdk.MetaspaceSummary 元空間定時統計事件
        2. jdk.MetaspaceAllocationFailure 元空間分配失敗事件
        3. jdk.MetaspaceOOM 元空間 OOM 事件
        4. jdk.MetaspaceGCThreshold 元空間 GC 閾值變化事件
        5. jdk.MetaspaceChunkFreeListSummary 元空間 Chunk FreeList 統計事件
  5. JVM 執行緒記憶體設計(重點研究 Java 執行緒)(全網最硬核 JVM 記憶體解析 - 13.JVM 執行緒記憶體設計開始)
    1. JVM 中有哪幾種執行緒,對應執行緒棧相關的引數是什麼(涉及 JVM 引數:ThreadStackSize,VMThreadStackSize,CompilerThreadStackSize,StackYellowPages,StackRedPages,StackShadowPages,StackReservedPages,RestrictReservedStack
    2. Java 執行緒棧記憶體的結構
    3. Java 執行緒如何丟擲的 StackOverflowError
      1. 解釋執行與編譯執行時候的判斷(x86為例)
      2. 一個 Java 執行緒 Xss 最小能指定多大

1. 從 Native Memory Tracking 說起

JVM 記憶體究竟包括哪些,可能網上眾說紛紜。我們這裡由官方提供的一個檢視 JVM 記憶體佔用的工具引入,即 Native Memory Tracking。不過要注意的一點是,這個只能監控 JVM 原生申請的記憶體大小,如果是透過 JDK 封裝的系統 API 申請的記憶體,是統計不到的,例如 Java JDK 中的 DirectBuffer 以及 MappedByteBuffer 這兩個(當然,對於這兩個,我們後面也有其他的辦法去看到當前使用的大小。當然xigao dog 啥都不會)。以及如果你自己封裝 JNI 呼叫系統呼叫去申請記憶體,都是 Native Memory Tracking 無法涵蓋的。這點要注意。

1.1. Native Memory Tracking 的開啟

Native Memory Tracking 主要是用來透過在 JVM 向系統申請記憶體的時候進行埋點實現的。注意,這個埋點,並不是完全沒有消耗的,我們後面會看到。由於需要埋點,並且 JVM 中申請記憶體的地方很多,這個埋點是有不小消耗的,這個 Native Memory Tracking 預設是不開啟的,並且無法動態開啟(因為這是埋點採集統計的,如果可以動態開啟那麼沒開啟的時候的記憶體分配沒有記錄無法知曉,所以無法動態開啟),目前只能透過在啟動 JVM 的時候透過啟動引數開啟。即透過 -XX:NativeMemoryTracking 開啟:

  • -XX:NativeMemoryTracking=off:這是預設值,即關閉 Native Memory Tracking
  • -XX:NativeMemoryTracking=summary: 開啟 Native Memory Tracking,但是僅僅按照各個 JVM 子系統去統計記憶體佔用情況
  • -XX:NativeMemoryTracking=detail:開啟 Native Memory Tracking,從每次 JVM 中申請記憶體的不同呼叫路徑的維度去統計記憶體佔用情況。注意,開啟 detail 比開啟 summary 的消耗要大不少,因為 detail 每次都要解析 CallSite 分辨呼叫位置。我們一般用不到這麼詳細的內容,除非是 JVM 開發。只有洗稿狗才會開啟這個配置導致線上崩潰而自己又很懵。

開啟之後,我們可以透過 jcmd 命令去檢視 Native Memory Tracking 的資訊,即jcmd <pid> VM.native_memory

  • jcmd <pid> VM.native_memory或者jcmd <pid> VM.native_memory summary:兩者是等價的,即檢視 Native Memory Tracking 的 summary 資訊。預設單位是 KB,可以指定單位為其他,例如 jcmd <pid> VM.native_memory summary scale=MB
  • jcmd <pid> VM.native_memory detail檢視 Native Memory Tracking 的 detail 資訊,包括 summary 資訊,以及按照虛擬記憶體對映分組的記憶體使用資訊,還有按照不同 CallSite 呼叫分組的記憶體使用情況。預設單位是 KB,可以指定單位為其他,例如 jcmd <pid> VM.native_memory detail scale=MB

1.2. Native Memory Tracking 的使用

對於我們這些 Java 開發以及 JVM 使用者而言(對於抄襲狗是沒有好果汁吃的),我們只關心並且檢視 Native Memory Tracking 的 summary 資訊即可,detail 資訊一般是供 JVM 開發人員使用的,我們不用太關心,我們後面的分析也只會涉及 Native Memory Tracking 的 summary 部分。

一般地,只有遇到問題的時候,我們才會考慮開啟 Native Memory Tracking,並且在定位出問題後,我們想把它關閉,可以透過 jcmd <pid> VM.native_memory shutdown 進行關閉並清理掉之前 Native Memory tracking 使用的埋點以及佔用的記憶體。如前面所述,我們無法動態開啟 Native Memory tracking,所以只要動態關閉了,這個程式就無法再開啟了。

jcmd 本身提供了簡單的對比功能,例如:

  1. 使用 jcmd <pid> VM.native_memory baseline 記錄當前記憶體佔用資訊
  2. 之後過一段時間 jcmd <pid> VM.native_memory summary.diff 會輸出當前 Native Memory Tracking 的 summary 資訊,如果與第一步 baseline 的有差異,會在對應位將差異輸出

但是這個工具本身比較粗糙,我們有時候並不知道何時呼叫 jcmd <pid> VM.native_memory summary.diff 合適,因為我們不確定什麼時候會有我們想看到的記憶體使用過大的問題。所以我們一般做成一種持續監控的方式

1.3. Native Memory Tracking 的 summary 資訊每部分含義

以下是一個 Native Memory Tracking 的示例輸出:

Total: reserved=10575644KB, committed=443024KB
-                 Java Heap (reserved=8323072KB, committed=192512KB)
                            (mmap: reserved=8323072KB, committed=192512KB) 
 
-                     Class (reserved=1050202KB, committed=10522KB)
                            (classes #15409)
                            (  instance classes #14405, array classes #1004)
                            (malloc=1626KB #33495) 
                            (mmap: reserved=1048576KB, committed=8896KB) 
                            (  Metadata:   )
                            (    reserved=57344KB, committed=57216KB)
                            (    used=56968KB)
                            (    waste=248KB =0.43%)
                            (  Class space:)
                            (    reserved=1048576KB, committed=8896KB)
                            (    used=8651KB)
                            (    waste=245KB =2.75%)
 
-                    Thread (reserved=669351KB, committed=41775KB)
                            (thread #653)
                            (stack: reserved=667648KB, committed=40072KB)
                            (malloc=939KB #3932) 
                            (arena=764KB #1304)
 
-                      Code (reserved=50742KB, committed=17786KB)
                            (malloc=1206KB #9495) 
                            (mmap: reserved=49536KB, committed=16580KB) 
 
-                        GC (reserved=370980KB, committed=69260KB)
                            (malloc=28516KB #8340) 
                            (mmap: reserved=342464KB, committed=40744KB) 
 
-                  Compiler (reserved=159KB, committed=159KB)
                            (malloc=29KB #813) 
                            (arena=131KB #3)
 
-                  Internal (reserved=1373KB, committed=1373KB)
                            (malloc=1309KB #6135) 
                            (mmap: reserved=64KB, committed=64KB) 
 
-                     Other (reserved=12348KB, committed=12348KB)
                            (malloc=12348KB #14) 
 
-                    Symbol (reserved=18629KB, committed=18629KB)
                            (malloc=16479KB #445877) 
                            (arena=2150KB #1)
 
-    Native Memory Tracking (reserved=8426KB, committed=8426KB)
                            (malloc=325KB #4777) 
                            (tracking overhead=8102KB)
 
-        Shared class space (reserved=12032KB, committed=12032KB)
                            (mmap: reserved=12032KB, committed=12032KB) 
 
-               Arena Chunk (reserved=187KB, committed=187KB)
                            (malloc=187KB) 
 
-                   Tracing (reserved=32KB, committed=32KB)
                            (arena=32KB #1)
 
-                   Logging (reserved=5KB, committed=5KB)
                            (malloc=5KB #216) 
 
-                 Arguments (reserved=31KB, committed=31KB)
                            (malloc=31KB #90) 
 
-                    Module (reserved=403KB, committed=403KB)
                            (malloc=403KB #2919) 
 
-                 Safepoint (reserved=8KB, committed=8KB)
                            (mmap: reserved=8KB, committed=8KB) 
 
-           Synchronization (reserved=56KB, committed=56KB)
                            (malloc=56KB #789) 
 
-            Serviceability (reserved=1KB, committed=1KB)
                            (malloc=1KB #18) 
 
-                 Metaspace (reserved=57606KB, committed=57478KB)
                            (malloc=262KB #180) 
                            (mmap: reserved=57344KB, committed=57216KB) 
 
-      String Deduplication (reserved=1KB, committed=1KB)
                            (malloc=1KB #8) 
 

我們接下來將上面的資訊按不同子系統分別簡單分析下其含義:

1.Java堆記憶體,所有 Java 物件分配佔用記憶體的來源,由 JVM GC 管理回收,這是我們在第三章會重點分析的:

    //堆記憶體佔用,reserve 了 8323072KB,當前 commit 了 192512KB 用於實際使用
    Java Heap (reserved=8323072KB, committed=192512KB) 
        //堆記憶體都是透過 mmap 系統呼叫方式分配的
        (mmap: reserved=8323072KB, committed=192512KB)
        //chao xi 可恥

2.元空間,JVM 將類檔案載入到記憶體中用於後續使用佔用的空間,注意是 JVM C++ 層面的記憶體佔用,主要包括類檔案中在 JVM 解析為 C++ 的 Klass 類以及相關元素。對應的 Java 反射類 Class 還是在堆記憶體空間中:

      //Class 是類元空間總佔用,reserve 了 1050202KB,當前 commit 了 10522KB 用於實際使用
      //總共 reserved 1050202KB = mmap reserved 1048576KB + malloc 1626KB
      //總共 committed 10522KB = mmap committed 8896KB + malloc 1626KB
      Class (reserved=1050202KB, committed=10522KB)
            (classes #15409) //一共載入了 15409 個類
            (  instance classes #14405, array classes #1004) //其中 14405 個實體類,1004 個陣列類
            (malloc=1626KB #33495) //透過 malloc 系統呼叫方式一共分配了 1626KB,一共呼叫了 33495 次 malloc
            (mmap: reserved=1048576KB, committed=8896KB) //透過 mmap 系統呼叫方式 reserve 了 1048576KB,當前 commit 了 8896KB 用於實際使用
            (  Metadata:   )//注意,MetaData 這塊不屬於類元空間,屬於資料元空間,後面第四章會詳細分析
            (    reserved=57344KB, committed=57216KB) //資料元空間當前 reserve 了 57344KB,commit 了 57216KB 用於實際使用
            (    used=56968KB) //但是實際從 MetaChunk 的角度去看使用,只有 56968KB 用於實際資料的分配,有 248KB 的浪費
            (    waste=248KB =0.43%)
            (  Class space:)
            (    reserved=1048576KB, committed=8896KB) //類元空間當前 reserve 了 1048576KB,commit 了 8896KB 用於實際使用
            (    used=8651KB) //但是實際從 MetaChunk 的角度去看使用,只有 8651KB 用於實際資料的分配,有 245KB 的浪費
            (    waste=245KB =2.75%)
            洗稿去shi
      Shared class space (reserved=12032KB, committed=12032KB) //共享類空間,當前 reserve 了 12032KB,commit 了 12032KB 用於實際使用,這塊其實屬於上面 Class 的一部分
            (mmap: reserved=12032KB, committed=12032KB) 
      Module (reserved=403KB, committed=403KB) //載入並記錄模組佔用空間,當前 reserve 了 403KB,commit 了 403KB 用於實際使用
            (malloc=403KB #2919) 
      Metaspace (reserved=57606KB, committed=57478KB) //等價於上面 Class 中的 MetaChunk(除了 malloc 的部分),當前 reserve 了 57606KB,commit 了 57478KB 用於實際使用
            (malloc=262KB #180) 
            (mmap: reserved=57344KB, committed=57216KB) 

3.C++ 字串即符號(Symbol)佔用空間,前面載入類的時候,其實裡面有很多字串資訊(注意不是 Java 字串,是 JVM 層面 C++ 字串),不同類的字串資訊可能會重複(維護原創打死潮汐犬)。所以統一放入符號表(Symbol table)複用。元空間中儲存的是針對符號表中符號的引用。這不是本期內容的重點,我們不會詳細分析

Symbol (reserved=18629KB, committed=18629KB)
(malloc=16479KB #445877) //透過 malloc 系統呼叫方式一共分配了 16479KB,一共呼叫了 445877 次 malloc
(arena=2150KB #1) //透過 arena 系統呼叫方式一共分配了 2150KB,一共呼叫了 1 次 arena

4.執行緒佔用記憶體,主要是每個執行緒的執行緒棧,我們也只會主要分析執行緒棧佔用空間(在第五章),其他的管理執行緒佔用的空間很小,可以忽略不計。

//總共 reserve 了 669351KB,commit 了 41775KB
Thread (reserved=669351KB, committed=41775KB)
    (thread #653)//當前執行緒數量是 653
    (stack: reserved=667648KB, committed=40072KB) //執行緒棧佔用的空間:我們沒有指定 Xss,預設是 1MB,所以 reserved 是 653 * 1024 = 667648KB,當前 commit 了 40072KB 用於實際使用
    (malloc=939KB #3932) //透過 malloc 系統呼叫方式一共分配了 939KB,一共呼叫了 3932 次 malloc
    (arena=764KB #1304)  //透過 JVM 內部 Arena 分配的記憶體,一共分配了 764KB,一共呼叫了 1304 次 Arena 分配

5.JIT編譯器本身佔用的空間以及JIT編譯器編譯後的程式碼佔用空間,這也不是本期內容的重點,我們不會詳細分析

Code (reserved=50742KB, committed=17786KB)
(malloc=1206KB #9495) 
(mmap: reserved=49536KB, committed=16580KB) 
//chao xi 直接去火葬場炒
Compiler (reserved=159KB, committed=159KB)
(malloc=29KB #813) 
(arena=131KB #3)   

6.Arena 資料結構佔用空間,我們看到 Native Memory Tracking 中有很多透過 arena 分配的記憶體,這個就是管理 Arena 資料結構佔用空間。這不是本期內容的重點,我們不會詳細分析

Arena Chunk (reserved=187KB, committed=187KB)
(malloc=187KB) 

7.JVM Tracing 佔用記憶體,包括 JVM perf 以及 JFR 佔用的空間。其中 JFR 佔用的空間可能會比較大,我在我的另一個關於 JFR 的系列裡面分析過 JVM 記憶體中佔用的空間。這不是本期內容的重點,我們不會詳細分析

Tracing (reserved=32KB, committed=32KB)
(arena=32KB #1)

8.寫 JVM 日誌佔用的記憶體-Xlog 引數指定的日誌輸出,並且 Java 17 之後引入了非同步 JVM 日誌-Xlog:async,非同步日誌所需的 buffer 也在這裡),這不是本期內容的重點,我們不會詳細分析

Logging (reserved=5KB, committed=5KB)
(malloc=5KB #216) 

9.JVM 引數佔用記憶體,我們需要儲存並處理當前的 JVM 引數以及使用者啟動 JVM 的是傳入的各種引數(有時候稱為 flag)。這不是本期內容的重點,我們不會詳細分析

Arguments (reserved=31KB, committed=31KB)
(malloc=31KB #90) 

10.JVM 安全點佔用記憶體,是固定的兩頁記憶體(我這裡是一頁是 4KB,後面第二章會分析這個頁大小與作業系統相關),用於 JVM 安全點的實現,不會隨著 JVM 執行時的記憶體佔用而變化。JVM 安全點請期待本系列文章的下一系列:全網最硬核的 JVM 安全點與執行緒握手機制解析。這不是本期內容的重點,我們不會詳細分析

Safepoint (reserved=8KB, committed=8KB)
(mmap: reserved=8KB, committed=8KB) 

11.Java 同步機制(例如 synchronized,還有 AQS 的基礎 LockSupport)底層依賴的 C++ 的資料結構,系統內部的 mutex 等佔用的記憶體。這不是本期內容的重點,我們不會詳細分析

Synchronization (reserved=56KB, committed=56KB)
(malloc=56KB #789)

12.JVM TI 相關記憶體,JVMTI 是 Java 虛擬機器工具介面(Java Virtual Machine Tool Interface)的縮寫。它是 Java 虛擬機器(JVM)的一部分,提供了一組 API,使開發人員可以開發自己的 Java 工具和代理程式,以監視、分析和除錯 Java 應用程式。JVMTI API 是一組 C/C++ 函式,可以透過 JVM TI Agent Library 和 JVM 進行互動。開發人員可以使用 JVMTI API 開發自己的 JVM 代理程式或工具,以監視和操作 Java 應用程式。例如,可以使用 JVMTI API 開發效能分析工具、程式碼覆蓋率工具、記憶體洩漏檢測工具等等。這裡的記憶體就是呼叫了 JVMTI API 之後 JVM 為了生成資料佔用的記憶體。這不是本期內容的重點,我們不會詳細分析

Serviceability (reserved=1KB, committed=1KB)
(malloc=1KB #18) 

13.Java 字串去重佔用記憶體:Java 字串去重機制可以減少應用程式中字串物件的記憶體佔用。 在 Java 應用程式中,字串常量是不可變的,並且通常被使用多次。這意味著在應用程式中可能存在大量相同的字串物件,這些物件佔用了大量的記憶體。Java 字串去重機制透過在堆中共享相同的字串物件來解決這個問題。當一個字串物件被建立時,JVM 會檢查堆中是否已經存在相同的字串物件。如果存在,那麼新的字串物件將被捨棄,而引用被返回給現有的物件。這樣就可以減少應用程式中字串物件的數量,從而減少記憶體佔用。 但是這個機制一直在某些 GC 下表現不佳,尤其是 G1GC 以及 ZGC 中,所以預設是關閉的,可以透過 -XX:+UseStringDeduplication 來啟用。這不是本期內容的重點,我們不會詳細分析。

String Deduplication (reserved=1KB, committed=1KB)
(malloc=1KB #8) 

14.JVM GC需要的資料結構與記錄資訊佔用的空間,這塊記憶體可能會比較大,尤其是對於那種專注於低延遲的 GC,例如 ZGC。其實 ZGC 是一種以空間換時間的思路,提高 CPU 消耗與記憶體佔用,但是消滅全域性暫停。之後的 ZGC 最佳化方向就是儘量降低 CPU 消耗與記憶體佔用,相當於提高了價效比。這不是本期內容的重點,我們不會詳細分析。

GC (reserved=370980KB, committed=69260KB)
(malloc=28516KB #8340) 
(mmap: reserved=342464KB, committed=40744KB) 

15.JVM內部(不屬於其他類的佔用就會歸到這一類)與其他佔用(不是 JVM 本身而是作業系統的某些系統呼叫導致額外佔的空間),不會很大

Internal (reserved=1373KB, committed=1373KB)
(malloc=1309KB #6135) 
(mmap: reserved=64KB, committed=64KB) 

Other (reserved=12348KB, committed=12348KB)
(malloc=12348KB #14) 

16.開啟 Native Memory Tracking 本身消耗的記憶體,這個就不用多說了吧

Native Memory Tracking (reserved=8426KB, committed=8426KB)
(malloc=325KB #4777) 
(tracking overhead=8102KB)

1.4. Native Memory Tracking 的 summary 資訊的持續監控

現在 JVM 一般大部分部署在 k8s 這種雲容器編排的環境中,每個 JVM 程式記憶體是受限的。如果超過限制,那麼會觸發 OOMKiller 將這個 JVM 程式殺掉。我們一般都是由於自己的 JVM 程式被 OOMKiller 殺掉,才會考慮開啟 NativeMemoryTracking 去看看哪塊記憶體佔用比較多以及如何調整的。

OOMKiller 是積分制,並不是你的 JVM 程式一超過限制就立刻會被殺掉,而是超過的話會累積分,累積到一定程度,就可能會被 OOMKiller 殺掉。所以,我們可以透過定時輸出 Native Memory Tracking 的 summary 資訊,從而抓到超過記憶體限制的點進行分析

但是,我們不能僅透過 Native Memory Tracking 的資料就判斷 JVM 佔用的記憶體,因為在後面的 JVM 記憶體申請與使用流程的分析我們會看到,JVM 透過 mmap 分配的大量記憶體都是先 reserve 再 commit 之後實際往裡面寫入資料的時候,才會真正分配實體記憶體。同時,JVM 還會動態釋放一些記憶體,這些記憶體可能不會立刻被作業系統回收。Native Memory Tracking 是 JVM 認為自己向作業系統申請的記憶體,與實際作業系統分配的記憶體是有所差距的,所以我們不能只檢視 Native Memory Tracking 去判斷,我們還需要檢視能體現真正記憶體佔用指標。這裡可以檢視 linux 程式監控檔案 smaps_rollup 看出具體的記憶體佔用,例如 (一般不看 Rss,因為如果涉及多個虛擬地址對映同一個實體地址的話會有不準確,所以主要關注 Pss 即可,但是 Pss 更新不是實時的,但也差不多,這就可以理解為程式佔用的實際實體記憶體):

> cat /proc/23/smaps_rollup 
689000000-fffff53a9000 ---p 00000000 00:00 0                             [rollup]
Rss:             5870852 kB
Pss:             5849120 kB
Pss_Anon:        5842756 kB
Pss_File:           6364 kB
Pss_Shmem:             0 kB
Shared_Clean:      27556 kB
Shared_Dirty:          0 kB
Private_Clean:       524 kB
Private_Dirty:   5842772 kB
Referenced:      5870148 kB
Anonymous:       5842756 kB
LazyFree:              0 kB
AnonHugePages:         0 kB
ShmemPmdMapped:        0 kB
FilePmdMapped:        0 kB
Shared_Hugetlb:        0 kB
Private_Hugetlb:       0 kB
Swap:                  0 kB
SwapPss:               0 kB
Locked:                0 kB

筆者透過在每個 Spring Cloud 微服務程式加入下面的程式碼,來實現定時的程式記憶體監控,主要透過 smaps_rollup 檢視實際的實體記憶體佔用找到記憶體超限的時間點,Native Memory Tracking 檢視 JVM 每塊記憶體佔用的多少,用於指導最佳化引數。

import lombok.extern.log4j.Log4j2;
import org.apache.commons.io.FileUtils;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

import static org.springframework.cloud.bootstrap.BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME;

@Log4j2
public class MonitorMemoryRSS implements ApplicationListener<ApplicationReadyEvent> {
    private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);

    private static final ScheduledThreadPoolExecutor sc = new ScheduledThreadPoolExecutor(1);


    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        if (isBootstrapContext(event)) {
            return;
        }
        synchronized (INITIALIZED) {
            if (INITIALIZED.get()) {
                return;
            }
            sc.scheduleAtFixedRate(() -> {
                long pid = ProcessHandle.current().pid();
                try {
                    //讀取 smaps_rollup
                    List<String> strings = FileUtils.readLines(new File("/proc/" + pid + "/smaps_rollup"));
                    log.info("MonitorMemoryRSS, smaps_rollup: {}", strings.stream().collect(Collectors.joining("\n")));
                    //讀取 Native Memory Tracking 資訊
                    Process process = Runtime.getRuntime().exec(new String[]{"jcmd", pid + "", "VM.native_memory"});
                    try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                        log.info("MonitorMemoryRSS, native_memory: {}", reader.lines().collect(Collectors.joining("\n")));
                    }
                } catch (IOException e) {
                }

            }, 0, 30, TimeUnit.SECONDS);
            INITIALIZED.set(true);
        }
    }

    static boolean isBootstrapContext(ApplicationReadyEvent applicationEvent) {
        return applicationEvent.getApplicationContext().getEnvironment().getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME);
    }
}

同時,筆者還將這些輸出抽象為 JFR 事件,效果是:

image

1.5. 為何 Native Memory Tracking 中申請的記憶體分為 reserved 和 committed

這個會在第二章詳細分析

相關文章