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 參數):
- 從 Native Memory Tracking 說起(全網最硬核 JVM 內存解析 - 1.從 Native Memory Tracking 說起開始)
- Native Memory Tracking 的開啓
- Native Memory Tracking 的使用(涉及 JVM 參數:
NativeMemoryTracking
) - Native Memory Tracking 的 summary 信息每部分含義
- Native Memory Tracking 的 summary 信息的持續監控
- 爲何 Native Memory Tracking 中申請的內存分爲 reserved 和 committed
- JVM 內存申請與使用流程(全網最硬核 JVM 內存解析 - 2.JVM 內存申請與使用流程開始)
- Linux 下內存管理模型簡述
- JVM commit 的內存與實際佔用內存的差異
- JVM commit 的內存與實際佔用內存的差異
- 大頁分配 UseLargePages(全網最硬核 JVM 內存解析 - 3.大頁分配 UseLargePages開始)
- Linux 大頁分配方式 - Huge Translation Lookaside Buffer Page (hugetlbfs)
- Linux 大頁分配方式 - Transparent Huge Pages (THP)
- JVM 大頁分配相關參數與機制(涉及 JVM 參數:
UseLargePages
,UseHugeTLBFS
,UseSHM
,UseTransparentHugePages
,LargePageSizeInBytes
)
- Java 堆內存相關設計(全網最硬核 JVM 內存解析 - 4.Java 堆內存大小的確認開始)
- 通用初始化與擴展流程
- 直接指定三個指標的方式(涉及 JVM 參數:
MaxHeapSize
,MinHeapSize
,InitialHeapSize
,Xmx
,Xms
) - 不手動指定三個指標的情況下,這三個指標(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何計算的
- 壓縮對象指針相關機制(涉及 JVM 參數:
UseCompressedOops
)(全網最硬核 JVM 內存解析 - 5.壓縮對象指針相關機制開始)- 壓縮對象指針存在的意義(涉及 JVM 參數:
ObjectAlignmentInBytes
) - 壓縮對象指針與壓縮類指針的關係演進(涉及 JVM 參數:
UseCompressedOops
,UseCompressedClassPointers
) - 壓縮對象指針的不同模式與尋址優化機制(涉及 JVM 參數:
ObjectAlignmentInBytes
,HeapBaseMinAddress
)
- 壓縮對象指針存在的意義(涉及 JVM 參數:
- 爲何預留第 0 頁,壓縮對象指針 null 判斷擦除的實現(涉及 JVM 參數:
HeapBaseMinAddress
) - 結合壓縮對象指針與前面提到的堆內存限制的初始化的關係(涉及 JVM 參數:
HeapBaseMinAddress
,ObjectAlignmentInBytes
,MinHeapSize
,MaxHeapSize
,InitialHeapSize
) - 使用 jol + jhsdb + JVM 日誌查看壓縮對象指針與 Java 堆驗證我們前面的結論
- 驗證
32-bit
壓縮指針模式 - 驗證
Zero based
壓縮指針模式 - 驗證
Non-zero disjoint
壓縮指針模式 - 驗證
Non-zero based
壓縮指針模式
- 驗證
- 堆大小的動態伸縮(涉及 JVM 參數:
MinHeapFreeRatio
,MaxHeapFreeRatio
,MinHeapDeltaBytes
)(全網最硬核 JVM 內存解析 - 6.其他 Java 堆內存相關的特殊機制開始) - 適用於長期運行並且儘量將所有可用內存被堆使用的 JVM 參數 AggressiveHeap
- JVM 參數 AlwaysPreTouch 的作用
- JVM 參數 UseContainerSupport - JVM 如何感知到容器內存限制
- JVM 參數 SoftMaxHeapSize - 用於平滑遷移更耗內存的 GC 使用
- JVM 元空間設計(全網最硬核 JVM 內存解析 - 7.元空間存儲的元數據開始)
- 什麼是元數據,爲什麼需要元數據
- 什麼時候用到元空間,元空間保存什麼
- 什麼時候用到元空間,以及釋放時機
- 元空間保存什麼
- 元空間的核心概念與設計(全網最硬核 JVM 內存解析 - 8.元空間的核心概念與設計開始)
- 元空間的整體配置以及相關參數(涉及 JVM 參數:
MetaspaceSize
,MaxMetaspaceSize
,MinMetaspaceExpansion
,MaxMetaspaceExpansion
,MaxMetaspaceFreeRatio
,MinMetaspaceFreeRatio
,UseCompressedClassPointers
,CompressedClassSpaceSize
,CompressedClassSpaceBaseAddress
,MetaspaceReclaimPolicy
) - 元空間上下文
MetaspaceContext
- 虛擬內存空間節點列表
VirtualSpaceList
- 虛擬內存空間節點
VirtualSpaceNode
與CompressedClassSpaceSize
MetaChunk
ChunkHeaderPool
池化MetaChunk
對象ChunkManager
管理空閒的MetaChunk
- 類加載的入口
SystemDictionary
與保留所有ClassLoaderData
的ClassLoaderDataGraph
- 每個類加載器私有的
ClassLoaderData
以及ClassLoaderMetaspace
- 管理正在使用的
MetaChunk
的MetaspaceArena
- 元空間內存分配流程(全網最硬核 JVM 內存解析 - 9.元空間內存分配流程開始)
- 類加載器到
MetaSpaceArena
的流程 - 從
MetaChunkArena
普通分配 - 整體流程 - 從
MetaChunkArena
普通分配 -FreeBlocks
回收老的current chunk
與用於後續分配的流程 - 從
MetaChunkArena
普通分配 - 嘗試從FreeBlocks
分配 - 從
MetaChunkArena
普通分配 - 嘗試擴容current chunk
- 從
MetaChunkArena
普通分配 - 從ChunkManager
分配新的MetaChunk
- 從
MetaChunkArena
普通分配 - 從ChunkManager
分配新的MetaChunk
- 從VirtualSpaceList
申請新的RootMetaChunk
- 從
MetaChunkArena
普通分配 - 從ChunkManager
分配新的MetaChunk
- 將RootMetaChunk
切割成爲需要的MetaChunk
MetaChunk
回收 - 不同情況下,MetaChunk
如何放入FreeChunkListVector
- 類加載器到
ClassLoaderData
回收
- 元空間的整體配置以及相關參數(涉及 JVM 參數:
- 元空間分配與回收流程舉例(全網最硬核 JVM 內存解析 - 10.元空間分配與回收流程舉例開始)
- 首先類加載器 1 需要分配 1023 字節大小的內存,屬於類空間
- 然後類加載器 1 還需要分配 1023 字節大小的內存,屬於類空間
- 然後類加載器 1 需要分配 264 KB 大小的內存,屬於類空間
- 然後類加載器 1 需要分配 2 MB 大小的內存,屬於類空間
- 然後類加載器 1 需要分配 128KB 大小的內存,屬於類空間
- 新來一個類加載器 2,需要分配 1023 Bytes 大小的內存,屬於類空間
- 然後類加載器 1 被 GC 回收掉
- 然後類加載器 2 需要分配 1 MB 大小的內存,屬於類空間
- 元空間大小限制與動態伸縮(全網最硬核 JVM 內存解析 - 11.元空間分配與回收流程舉例開始)
CommitLimiter
的限制元空間可以 commit 的內存大小以及限制元空間佔用達到多少就開始嘗試 GC- 每次 GC 之後,也會嘗試重新計算
_capacity_until_GC
jcmd VM.metaspace
元空間說明、元空間相關 JVM 日誌以及元空間 JFR 事件詳解(全網最硬核 JVM 內存解析 - 12.元空間各種監控手段開始)jcmd <pid> VM.metaspace
元空間說明- 元空間相關 JVM 日誌
- 元空間 JFR 事件詳解
jdk.MetaspaceSummary
元空間定時統計事件jdk.MetaspaceAllocationFailure
元空間分配失敗事件jdk.MetaspaceOOM
元空間 OOM 事件jdk.MetaspaceGCThreshold
元空間 GC 閾值變化事件jdk.MetaspaceChunkFreeListSummary
元空間 Chunk FreeList 統計事件
- JVM 線程內存設計(重點研究 Java 線程)(全網最硬核 JVM 內存解析 - 13.JVM 線程內存設計開始)
- JVM 中有哪幾種線程,對應線程棧相關的參數是什麼(涉及 JVM 參數:
ThreadStackSize
,VMThreadStackSize
,CompilerThreadStackSize
,StackYellowPages
,StackRedPages
,StackShadowPages
,StackReservedPages
,RestrictReservedStack
) - Java 線程棧內存的結構
- Java 線程如何拋出的 StackOverflowError
- 解釋執行與編譯執行時候的判斷(x86爲例)
- 一個 Java 線程 Xss 最小能指定多大
- JVM 中有哪幾種線程,對應線程棧相關的參數是什麼(涉及 JVM 參數:
3. Java 堆內存相關設計
3.1. 通用初始化與擴展流程
目前最新的 JVM,主要根據三個指標初始化堆以及擴展或縮小堆:
- 最大堆大小
- 最小堆大小
- 初始堆大小
不同的 GC 情況下,初始化以及擴展的流程可能在某些細節不太一樣,但是,大體的思路都是:
- 初始化階段,reserve 最大堆大小,並且 commit 初始堆大小
- 在某些 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
:
#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
:
//如果設置了 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 啓動參數,同一個參數可以多次出現,但是隻有最後一個會生效,例如:
java -XX:MaxHeapSize=8G -XX:MaxHeapSize=4G -XX:MaxHeapSize=8M -version
這個命令啓動的 JVM MaxHeapSize 爲 8MB。由於前面提到 Xmx 與 MaxHeapSize 是等價的,所以這麼寫也是可以的(雖然最後 MaxHeapSize 還是 8MB):
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
或者 Xmx
,MaxHeapSize
也有自己預設的一個參考值。源碼中這個預設參考值爲 125MB 左右(96M*13/10
)。但是一般最後不會以這個參考值爲準,JVM 初始化的時候會有很複雜的計算計算出合適的值。比如你可以在你的電腦上執行下下面的命令,可以看到類似下面的輸出:
> 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 的計算流程:
流程中涉及了以下幾個參數,還有一些已經過期的參數,會被轉換成未過期的參數:
MinRAMPercentage
:注意不要被名字迷惑,這個參數是在可用內存比較小的時候生效,即最大堆內存佔用爲可用內存的這個參數指定的百分比,默認爲 50,即 50%MaxRAMPercentage
:注意不要被名字迷惑,這個參數是在可用內存比較大的時候生效,即最大堆內存佔用爲可用內存的這個參數指定的百分比,默認爲 25,即 25%ErgoHeapSizeLimit
:通過自動計算,計算出的最大堆內存大小不超過這個參數指定的大小,默認爲 0 即不限制MinRAMFraction
: 已過期,如果配置了會轉化爲MinRAMPercentage
換算關係是:MinRAMPercentage
= 100.0 /MinRAMFraction
,默認是 2MaxRAMFraction
: 已過期,如果配置了會轉化爲MaxRAMPercentage
換算關係是:MaxRAMPercentage
= 100.0 /MaxRAMFraction
,默認是 4
對應的源碼是:https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/share/gc/shared/gc_globals.hpp
:
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
,也會經過下面的計算過程計算出來:
流程中涉及了以下幾個參數,還有一些已經過期的參數,會被轉換成未過期的參數:
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
:
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) \