[轉帖]全網最硬核 JVM 內存解析 - 4.Java 堆內存大小的確認

https://cloud.tencent.com/developer/article/2277323

 

個人創作公約:本人聲明創作的所有文章皆爲自己原創,如果有參考任何文章的地方,會標註出來,如果有疏漏,歡迎大家批判。如果大家發現網上有抄襲本文章的,歡迎舉報,並且積極向這個 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 最小能指定多大

3. Java 堆內存相關設計

3.1. 通用初始化與擴展流程

目前最新的 JVM,主要根據三個指標初始化堆以及擴展或縮小堆:

  • 最大堆大小
  • 最小堆大小
  • 初始堆大小

不同的 GC 情況下,初始化以及擴展的流程可能在某些細節不太一樣,但是,大體的思路都是:

  1. 初始化階段,reserve 最大堆大小,並且 commit 初始堆大小
  2. 在某些 GC 的某些階段,根據上次 GC 的數據,動態擴展或者縮小堆大小,擴展就是 commit 更多,縮小就是 uncommit 一部分內存。但是,堆大小不會小於最小堆大小,也不會大於最大堆大小

3.2. 直接指定三個指標(MinHeapSize,MaxHeapSize,InitialHeapSize)的方式

這三個指標,直接對應的 JVM 參數是:

  • 最大堆大小MaxHeapSize,如果沒有指定的話會有默認預設值用於指導 JVM 計算這些指標的大小,下一章節會詳細分析,預設值爲 125MB 左右(96M*13/10)
  • 最小堆大小MinHeapSize,默認爲 0,0 代表讓 JVM 自己計算,下一章節會詳細分析
  • 初始堆大小InitialHeapSize,默認爲 0,0 代表讓 JVM 自己計算,下一章節會詳細分析

對應源碼是:https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/gc/shared/gc_globals.hpp

代碼語言:javascript
複製
#define ScaleForWordSize(x) align_down((x) * 13 / 10, HeapWordSize)

product(size_t, MaxHeapSize, ScaleForWordSize(96*M),                \
  "Maximum heap size (in bytes)")                                   \
  constraint(MaxHeapSizeConstraintFunc,AfterErgo)                   \
product(size_t, MinHeapSize, 0,                                     \
  "Minimum heap size (in bytes); zero means use ergonomics")        \
  constraint(MinHeapSizeConstraintFunc,AfterErgo)                   \
product(size_t, InitialHeapSize, 0,                                 \
  "Initial heap size (in bytes); zero means use ergonomics")        \
  constraint(InitialHeapSizeConstraintFunc,AfterErgo)               \

我們可以通過類似於 -XX:MaxHeapSize=1G 這種啓動參數對這三個指標進行設置,但是,我們經常看到的可能是 Xmx 以及 Xms 這兩個參數設置這三個指標,這兩個參數分別對應:

  • Xmx:對應 最大堆大小 等價於 MaxHeapSize
  • Xms:相當於同時設置最小堆大小 MinHeapSize初始堆大小 InitialHeapSize

對應的 JVM 源碼是:https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/runtime/arguments.cpp

代碼語言:javascript
複製
//如果設置了 Xms
else if (match_option(option, "-Xms", &tail)) {
  julong size = 0;
  //解析 Xms 大小
  ArgsRange errcode = parse_memory_size(tail, &size, 0);
  if (errcode != arg_in_range) {
    jio_fprintf(defaultStream::error_stream(),
                "Invalid initial heap size: %s\n", option->optionString);
    describe_range_error(errcode);
    return JNI_EINVAL;
  }
  //將解析的值設置到 MinHeapSize
  if (FLAG_SET_CMDLINE(MinHeapSize, (size_t)size) != JVMFlag::SUCCESS) {
    return JNI_EINVAL;
  }
  //將解析的值設置到 InitialHeapSize
  if (FLAG_SET_CMDLINE(InitialHeapSize, (size_t)size) != JVMFlag::SUCCESS) {
    return JNI_EINVAL;
  }
//如果設置了 Xmx
} else if (match_option(option, "-Xmx", &tail) || match_option(option, "-XX:MaxHeapSize=", &tail)) {
  julong long_max_heap_size = 0;
  //解析 Xmx 大小
  ArgsRange errcode = parse_memory_size(tail, &long_max_heap_size, 1);
  if (errcode != arg_in_range) {
    jio_fprintf(defaultStream::error_stream(),
                "Invalid maximum heap size: %s\n", option->optionString);
    describe_range_error(errcode);
    return JNI_EINVAL;
  }
  //將解析的值設置到 MaxHeapSize
  if (FLAG_SET_CMDLINE(MaxHeapSize, (size_t)long_max_heap_size) != JVMFlag::SUCCESS) {
    return JNI_EINVAL;
  }
}

最後提一句,JVM 啓動參數,同一個參數可以多次出現,但是隻有最後一個會生效,例如:

代碼語言:javascript
複製
java -XX:MaxHeapSize=8G -XX:MaxHeapSize=4G -XX:MaxHeapSize=8M -version

這個命令啓動的 JVM MaxHeapSize 爲 8MB。由於前面提到 Xmx 與 MaxHeapSize 是等價的,所以這麼寫也是可以的(雖然最後 MaxHeapSize 還是 8MB):

代碼語言:javascript
複製
java -Xmx=8G -XX:MaxHeapSize=4G -XX:MaxHeapSize=8M -version

3.3. 不手動指定三個指標的情況下,這三個指標(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何計算的

上一章節我們提到我們可以手動指定這三個參數,如果不指定呢?JVM 會怎麼計算這三個指標的大小?首先,當然,JVM 會讀取 JVM 可用內存:首先 JVM 需要知道自己可用多少內存,我們稱爲可用內存。由此引入第一個 JVM 參數,MaxRAM,這個參數是用來明確指定 JVM 進程可用內存大小的,如果沒有指定,JVM 會自己讀取系統可用內存。這個可用內存用來指導 JVM 限制最大堆內存。後面我們會看到很多 JVM 參數與這個可用內存相關。

前面我們還提到了,就算沒有指定 MaxHeapSize 或者 XmxMaxHeapSize 也有自己預設的一個參考值。源碼中這個預設參考值爲 125MB 左右(96M*13/10)。但是一般最後不會以這個參考值爲準,JVM 初始化的時候會有很複雜的計算計算出合適的值。比如你可以在你的電腦上執行下下面的命令,可以看到類似下面的輸出:

代碼語言:javascript
複製
>  java -XX:+UnlockDiagnosticVMOptions -XX:+PrintFlagsFinal -version|grep MaxHeapSize
   size_t MaxHeapSize                              = 1572864000                                {product} {ergonomic}
   size_t SoftMaxHeapSize                          = 1572864000                             {manageable} {ergonomic}
openjdk version "17.0.2" 2022-01-18 LTS
OpenJDK Runtime Environment Corretto-17.0.2.8.1 (build 17.0.2+8-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.2.8.1 (build 17.0.2+8-LTS, mixed mode, sharing)

可以看到 MaxHeapSize 的大小,以及它的值是通過 ergonomic 決定的。也就是非人工指定而是 JVM 自己算出來的。

上面提到的那個 125MB 左右的初始參考值,一般用於 JVM 計算。我們接下來就會分析這個計算流程,首先是最大堆內存 MaxHeapSize 的計算流程:

image
image

流程中涉及了以下幾個參數,還有一些已經過期的參數,會被轉換成未過期的參數:

  • MinRAMPercentage:注意不要被名字迷惑,這個參數是在可用內存比較小的時候生效,即最大堆內存佔用爲可用內存的這個參數指定的百分比,默認爲 50,即 50%
  • MaxRAMPercentage:注意不要被名字迷惑,這個參數是在可用內存比較大的時候生效,即最大堆內存佔用爲可用內存的這個參數指定的百分比,默認爲 25,即 25%
  • ErgoHeapSizeLimit:通過自動計算,計算出的最大堆內存大小不超過這個參數指定的大小,默認爲 0 即不限制
  • MinRAMFraction: 已過期,如果配置了會轉化爲 MinRAMPercentage 換算關係是:MinRAMPercentage = 100.0 / MinRAMFraction,默認是 2
  • MaxRAMFraction: 已過期,如果配置了會轉化爲 MaxRAMPercentage 換算關係是:MaxRAMPercentage = 100.0 / MaxRAMFraction,默認是 4

對應的源碼是:https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/gc/shared/gc_globals.hpp

代碼語言:javascript
複製
product(double, MinRAMPercentage, 50.0,                             \
  "Minimum percentage of real memory used for maximum heap"         \
  "size on systems with small physical memory size")                \
  range(0.0, 100.0)                                                 \
product(double, MaxRAMPercentage, 25.0,                             \
  "Maximum percentage of real memory used for maximum heap size")   \
  range(0.0, 100.0)                                                 \
product(size_t, ErgoHeapSizeLimit, 0,                               \
  "Maximum ergonomically set heap size (in bytes); zero means use " \
  "MaxRAM * MaxRAMPercentage / 100")                                \
  range(0, max_uintx)                                               \
product(uintx, MinRAMFraction, 2,                                   \
  "Minimum fraction (1/n) of real memory used for maximum heap "    \
  "size on systems with small physical memory size. "               \
  "Deprecated, use MinRAMPercentage instead")                       \
  range(1, max_uintx)                                               \
product(uintx, MaxRAMFraction, 4,                                   \
  "Maximum fraction (1/n) of real memory used for maximum heap "    \
  "size. "                                                          \
  "Deprecated, use MaxRAMPercentage instead")                       \
  range(1, max_uintx)                                               \

然後如果我們也沒有設置 MinHeapSize 以及 InitialHeapSize,也會經過下面的計算過程計算出來:

image
image

流程中涉及了以下幾個參數,還有一些已經過期的參數,會被轉換成未過期的參數:

  • NewSize:初始新生代大小,預設值爲 1.3MB 左右(1*13/10
  • OldSize:老年代大小,預設值爲 5.2 MB 左右(4*13/10
  • InitialRAMPercentage:初始堆內存爲可用內存的這個參數指定的百分比,默認爲 1.5625,即 1.5625%
  • InitialRAMFraction: 已過期,如果配置了會轉化爲 InitialRAMPercentage 換算關係是:InitialRAMPercentage = 100.0 / InitialRAMFraction

對應的源碼是:https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/gc/shared/gc_globals.hpp

代碼語言:javascript
複製
product(size_t, NewSize, ScaleForWordSize(1*M),                     \
  "Initial new generation size (in bytes)")                         \
  constraint(NewSizeConstraintFunc,AfterErgo)                       \
product(size_t, OldSize, ScaleForWordSize(4*M),                     \
  "Initial tenured generation size (in bytes)")                     \
  range(0, max_uintx)                                               \
product(double, InitialRAMPercentage, 1.5625,                       \
  "Percentage of real memory used for initial heap size")           \
  range(0.0, 100.0)                                                 \
product(uintx, InitialRAMFraction, 64,                              \
  "Fraction (1/n) of real memory used for initial heap size. "      \
  "Deprecated, use InitialRAMPercentage instead")                   \
  range(1, max_uintx)                                               \
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章