全網(wǎng)最硬核 JVM 內(nèi)存解析 - 6.其他 Java 堆內(nèi)存相關(guān)的特殊機(jī)制

個(gè)人創(chuàng)作公約:本人聲明創(chuàng)作的所有文章皆為自己原創(chuàng),如果有參考任何文章的地方,會(huì)標(biāo)注出來(lái),如果有疏漏,歡迎大家批判。如果大家發(fā)現(xiàn)網(wǎng)上有抄襲本文章的,歡迎舉報(bào),并且積極向這個(gè)?github 倉(cāng)庫(kù)?提交 issue,謝謝支持~
另外,本文為了避免抄襲,會(huì)在不影響閱讀的情況下,在文章的隨機(jī)位置放入對(duì)于抄襲和洗稿的人的“親切”的問(wèn)候。如果是正常讀者看到,筆者在這里說(shuō)聲對(duì)不起,。如果被抄襲狗或者洗稿狗看到了,希望你能夠好好反思,不要再抄襲了,謝謝。
今天又是干貨滿滿的一天,這是全網(wǎng)最硬核 JVM 解析系列第四篇,往期精彩:
全網(wǎng)最硬核 TLAB 解析
全網(wǎng)最硬核 Java 隨機(jī)數(shù)解析
全網(wǎng)最硬核 Java 新內(nèi)存模型解析
本篇是關(guān)于 JVM 內(nèi)存的詳細(xì)分析。網(wǎng)上有很多關(guān)于 JVM 內(nèi)存結(jié)構(gòu)的分析以及圖片,但是由于不是一手的資料亦或是人云亦云導(dǎo)致有很錯(cuò)誤,造成了很多誤解;并且,這里可能最容易混淆的是一邊是 JVM Specification 的定義,一邊是 Hotspot JVM 的實(shí)際實(shí)現(xiàn),有時(shí)候人們一些部分說(shuō)的是 JVM Specification,一部分說(shuō)的是 Hotspot 實(shí)現(xiàn),給人一種割裂感與誤解。本篇主要從 Hotspot 實(shí)現(xiàn)出發(fā),以 Linux x86 環(huán)境為主,緊密貼合 JVM 源碼并且輔以各種 JVM 工具驗(yàn)證幫助大家理解 JVM 內(nèi)存的結(jié)構(gòu)。但是,本篇僅限于對(duì)于這些內(nèi)存的用途,使用限制,相關(guān)參數(shù)的分析,有些地方可能比較深入,有些地方可能需要結(jié)合本身用這塊內(nèi)存涉及的 JVM 模塊去說(shuō),會(huì)放在另一系列文章詳細(xì)描述。最后,洗稿抄襲狗不得 house
本篇全篇目錄(以及涉及的 JVM 參數(shù)):
從 Native Memory Tracking 說(shuō)起(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 1.從 Native Memory Tracking 說(shuō)起開(kāi)始)
Native Memory Tracking 的開(kāi)啟
Native Memory Tracking 的使用(涉及 JVM 參數(shù):
NativeMemoryTracking
)Native Memory Tracking 的 summary 信息每部分含義
Native Memory Tracking 的 summary 信息的持續(xù)監(jiān)控
為何 Native Memory Tracking 中申請(qǐng)的內(nèi)存分為 reserved 和 committed
JVM 內(nèi)存申請(qǐng)與使用流程(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 2.JVM 內(nèi)存申請(qǐng)與使用流程開(kāi)始)
Linux 大頁(yè)分配方式 - Huge Translation Lookaside Buffer Page (hugetlbfs)
Linux 大頁(yè)分配方式 - Transparent Huge Pages (THP)
JVM 大頁(yè)分配相關(guān)參數(shù)與機(jī)制(涉及 JVM 參數(shù):
UseLargePages
,UseHugeTLBFS
,UseSHM
,UseTransparentHugePages
,LargePageSizeInBytes
)JVM commit 的內(nèi)存與實(shí)際占用內(nèi)存的差異
Linux 下內(nèi)存管理模型簡(jiǎn)述
JVM commit 的內(nèi)存與實(shí)際占用內(nèi)存的差異
大頁(yè)分配 UseLargePages(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 3.大頁(yè)分配 UseLargePages開(kāi)始)
Java 堆內(nèi)存相關(guān)設(shè)計(jì)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 4.Java 堆內(nèi)存大小的確認(rèn)開(kāi)始)
驗(yàn)證?
32-bit
?壓縮指針模式驗(yàn)證?
Zero based
?壓縮指針模式驗(yàn)證?
Non-zero disjoint
?壓縮指針模式驗(yàn)證?
Non-zero based
?壓縮指針模式壓縮對(duì)象指針存在的意義(涉及 JVM 參數(shù):
ObjectAlignmentInBytes
)壓縮對(duì)象指針與壓縮類指針的關(guān)系演進(jìn)(涉及 JVM 參數(shù):
UseCompressedOops
,UseCompressedClassPointers
)壓縮對(duì)象指針的不同模式與尋址優(yōu)化機(jī)制(涉及 JVM 參數(shù):
ObjectAlignmentInBytes
,HeapBaseMinAddress
)通用初始化與擴(kuò)展流程
直接指定三個(gè)指標(biāo)的方式(涉及 JVM 參數(shù):
MaxHeapSize
,MinHeapSize
,InitialHeapSize
,Xmx
,Xms
)不手動(dòng)指定三個(gè)指標(biāo)的情況下,這三個(gè)指標(biāo)(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何計(jì)算的
壓縮對(duì)象指針相關(guān)機(jī)制(涉及 JVM 參數(shù):
UseCompressedOops
)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 5.壓縮對(duì)象指針相關(guān)機(jī)制開(kāi)始)為何預(yù)留第 0 頁(yè),壓縮對(duì)象指針 null 判斷擦除的實(shí)現(xiàn)(涉及 JVM 參數(shù):
HeapBaseMinAddress
)結(jié)合壓縮對(duì)象指針與前面提到的堆內(nèi)存限制的初始化的關(guān)系(涉及 JVM 參數(shù):
HeapBaseMinAddress
,ObjectAlignmentInBytes
,MinHeapSize
,MaxHeapSize
,InitialHeapSize
)使用 jol + jhsdb + JVM 日志查看壓縮對(duì)象指針與 Java 堆驗(yàn)證我們前面的結(jié)論
堆大小的動(dòng)態(tài)伸縮(涉及 JVM 參數(shù):
MinHeapFreeRatio
,MaxHeapFreeRatio
,MinHeapDeltaBytes
)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 6.其他 Java 堆內(nèi)存相關(guān)的特殊機(jī)制開(kāi)始)適用于長(zhǎng)期運(yùn)行并且盡量將所有可用內(nèi)存被堆使用的 JVM 參數(shù) AggressiveHeap
JVM 參數(shù) AlwaysPreTouch 的作用
JVM 參數(shù) UseContainerSupport - JVM 如何感知到容器內(nèi)存限制
JVM 參數(shù) SoftMaxHeapSize - 用于平滑遷移更耗內(nèi)存的 GC 使用
JVM 元空間設(shè)計(jì)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 7.元空間存儲(chǔ)的元數(shù)據(jù)開(kāi)始)
jcmd <pid> VM.metaspace
?元空間說(shuō)明元空間相關(guān) JVM 日志
元空間 JFR 事件詳解
jdk.MetaspaceSummary
?元空間定時(shí)統(tǒng)計(jì)事件jdk.MetaspaceAllocationFailure
?元空間分配失敗事件jdk.MetaspaceOOM
?元空間 OOM 事件jdk.MetaspaceGCThreshold
?元空間 GC 閾值變化事件jdk.MetaspaceChunkFreeListSummary
?元空間 Chunk FreeList 統(tǒng)計(jì)事件CommitLimiter
?的限制元空間可以 commit 的內(nèi)存大小以及限制元空間占用達(dá)到多少就開(kāi)始嘗試 GC每次 GC 之后,也會(huì)嘗試重新計(jì)算?
_capacity_until_GC
首先類加載器 1 需要分配 1023 字節(jié)大小的內(nèi)存,屬于類空間
然后類加載器 1 還需要分配 1023 字節(jié)大小的內(nèi)存,屬于類空間
然后類加載器 1 需要分配 264 KB 大小的內(nèi)存,屬于類空間
然后類加載器 1 需要分配 2 MB 大小的內(nèi)存,屬于類空間
然后類加載器 1 需要分配 128KB 大小的內(nèi)存,屬于類空間
新來(lái)一個(gè)類加載器 2,需要分配 1023 Bytes 大小的內(nèi)存,屬于類空間
然后類加載器 1 被 GC 回收掉
然后類加載器 2 需要分配 1 MB 大小的內(nèi)存,屬于類空間
元空間的整體配置以及相關(guān)參數(shù)(涉及 JVM 參數(shù):
MetaspaceSize
,MaxMetaspaceSize
,MinMetaspaceExpansion
,MaxMetaspaceExpansion
,MaxMetaspaceFreeRatio
,MinMetaspaceFreeRatio
,UseCompressedClassPointers
,CompressedClassSpaceSize
,CompressedClassSpaceBaseAddress
,MetaspaceReclaimPolicy
)元空間上下文?
MetaspaceContext
虛擬內(nèi)存空間節(jié)點(diǎn)列表?
VirtualSpaceList
虛擬內(nèi)存空間節(jié)點(diǎn)?
VirtualSpaceNode
?與?CompressedClassSpaceSize
MetaChunk
類加載的入口?
SystemDictionary
?與保留所有?ClassLoaderData
?的?ClassLoaderDataGraph
每個(gè)類加載器私有的?
ClassLoaderData
?以及?ClassLoaderMetaspace
管理正在使用的?
MetaChunk
?的?MetaspaceArena
元空間內(nèi)存分配流程(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 9.元空間內(nèi)存分配流程開(kāi)始)
ClassLoaderData
?回收ChunkHeaderPool
?池化?MetaChunk
?對(duì)象ChunkManager
?管理空閑的?MetaChunk
類加載器到?
MetaSpaceArena
?的流程從?
MetaChunkArena
?普通分配 - 整體流程從?
MetaChunkArena
?普通分配 -?FreeBlocks
?回收老的?current chunk
?與用于后續(xù)分配的流程從?
MetaChunkArena
?普通分配 - 嘗試從?FreeBlocks
?分配從?
MetaChunkArena
?普通分配 - 嘗試擴(kuò)容?current chunk
從?
MetaChunkArena
?普通分配 - 從?ChunkManager
?分配新的?MetaChunk
從?
MetaChunkArena
?普通分配 - 從?ChunkManager
?分配新的?MetaChunk
?- 從?VirtualSpaceList
?申請(qǐng)新的?RootMetaChunk
從?
MetaChunkArena
?普通分配 - 從?ChunkManager
?分配新的?MetaChunk
?- 將?RootMetaChunk
?切割成為需要的?MetaChunk
MetaChunk
?回收 - 不同情況下,?MetaChunk
?如何放入?FreeChunkListVector
什么時(shí)候用到元空間,以及釋放時(shí)機(jī)
元空間保存什么
什么是元數(shù)據(jù),為什么需要元數(shù)據(jù)
什么時(shí)候用到元空間,元空間保存什么
元空間的核心概念與設(shè)計(jì)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 8.元空間的核心概念與設(shè)計(jì)開(kāi)始)
元空間分配與回收流程舉例(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 10.元空間分配與回收流程舉例開(kāi)始)
元空間大小限制與動(dòng)態(tài)伸縮(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 11.元空間分配與回收流程舉例開(kāi)始)
jcmd VM.metaspace
?元空間說(shuō)明、元空間相關(guān) JVM 日志以及元空間 JFR 事件詳解(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 12.元空間各種監(jiān)控手段開(kāi)始)JVM 線程內(nèi)存設(shè)計(jì)(重點(diǎn)研究 Java 線程)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 13.JVM 線程內(nèi)存設(shè)計(jì)開(kāi)始)
解釋執(zhí)行與編譯執(zhí)行時(shí)候的判斷(x86為例)
一個(gè) Java 線程 Xss 最小能指定多大
JVM 中有哪幾種線程,對(duì)應(yīng)線程棧相關(guān)的參數(shù)是什么(涉及 JVM 參數(shù):
ThreadStackSize
,VMThreadStackSize
,CompilerThreadStackSize
,StackYellowPages
,StackRedPages
,StackShadowPages
,StackReservedPages
,RestrictReservedStack
)Java 線程棧內(nèi)存的結(jié)構(gòu)
Java 線程如何拋出的 StackOverflowError
3. Java 堆內(nèi)存相關(guān)設(shè)計(jì)
3.8. 堆大小的動(dòng)態(tài)伸縮
不同的 GC 堆大小動(dòng)態(tài)伸縮有很大很大的差異(比如 ParallelGC 涉及 UseAdaptiveSizePolicy 啟用的動(dòng)態(tài)堆大小策略以及相關(guān)的 UsePSAdaptiveSurvivorSizePolicy、UseAdaptiveGenerationSizePolicyAtMinorCollection 等等等等的參數(shù)參與決定計(jì)算最新堆大小的方式以及時(shí)機(jī)),在這個(gè)系列以后的章節(jié)我們?cè)敿?xì)分析每個(gè) GC 的時(shí)候再詳細(xì)分析這些不同 GC 的動(dòng)態(tài)伸縮策略。我們這里僅涉及大多數(shù) GC 通用的堆大小伸縮涉及的參數(shù):MinHeapFreeRatio
?與?MaxHeapFreeRatio
:
MinHeapFreeRatio
:目標(biāo)最小堆空閑比例,如果某次 GC 之后堆的某個(gè)區(qū)域(在某些 GC 是整個(gè)堆)空閑比例小于這個(gè)比例,那么就考慮將這個(gè)區(qū)域擴(kuò)容。默認(rèn)是 40,即默認(rèn)是 40%,但是某些 GC 如果你不設(shè)置就會(huì)變成 0%。0% 代表從來(lái)不會(huì)因?yàn)闆](méi)有達(dá)到目標(biāo)最小堆空閑比例而擴(kuò)容,配置為 0% 一般是為了堆的大小穩(wěn)定。MaxHeapFreeRatio
:目標(biāo)最大堆空閑比例,如果某次 GC 之后堆的某個(gè)區(qū)域(在某些 GC 是整個(gè)堆)空閑比例大于這個(gè)比例,那么就考慮將這個(gè)區(qū)域縮小。默認(rèn)是 70,即默認(rèn)是 70%,但是某些 GC 如果你不設(shè)置就會(huì)變成 100%。100% 代表從來(lái)不會(huì)因?yàn)闆](méi)有達(dá)到目標(biāo)最大堆空閑比例而擴(kuò)容,配置為 100% 一般是為了堆的大小穩(wěn)定。MinHeapDeltaBytes
:當(dāng)擴(kuò)容時(shí),至少擴(kuò)展多大的內(nèi)存。默認(rèn)是 166.4 KB(128*13/10
)
對(duì)應(yīng)的源碼是:https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/runtime/globals.hpp
:
product(uintx, MinHeapFreeRatio, 40, MANAGEABLE, ? ? ? ? ? ? ? ? ? ?\
?"The minimum percentage of heap free after GC to avoid expansion."\
?" For most GCs this applies to the old generation. In G1 and" ? ? \
?" ParallelGC it applies to the whole heap.") ? ? ? ? ? ? ? ? ? ? ?\
?range(0, 100) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
?constraint(MinHeapFreeRatioConstraintFunc,AfterErgo) ? ? ? ? ? ? ?\
product(uintx, MaxHeapFreeRatio, 70, MANAGEABLE, ? ? ? ? ? ? ? ? ? ?\
?"The maximum percentage of heap free after GC to avoid shrinking."\
?" For most GCs this applies to the old generation. In G1 and" ? ? \
?" ParallelGC it applies to the whole heap.") ? ? ? ? ? ? ? ? ? ? ?\
?range(0, 100) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
?constraint(MaxHeapFreeRatioConstraintFunc,AfterErgo) ? ? ? ? ? ? ?\
product(size_t, MinHeapDeltaBytes, ScaleForWordSize(128*K), ? ? ? ? \
?"The minimum change in heap space due to GC (in bytes)") ? ? ? ? ?\
?range(0, max_uintx) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? \
這兩個(gè)參數(shù),在不同 GC 下的實(shí)際表現(xiàn),如下:
SerialGC:在 SerialGC 的情況下,
MinHeapFreeRatio
?與?MaxHeapFreeRatio
?指的僅僅是老年代的目標(biāo)空閑比例,僅對(duì)老年代生效。在觸發(fā)涉及老年代的 GC 的時(shí)候(其實(shí)就是 FullGC),GC 結(jié)束時(shí),會(huì)查看(抄襲和xigao是文化的毒瘤,是對(duì)文化創(chuàng)造和發(fā)展的阻礙)當(dāng)前老年代的空閑比例,與?MinHeapFreeRatio
?和?MaxHeapFreeRatio
比較 判斷是否擴(kuò)容或者縮小老年代的大小(這里的源碼參考:https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/gc/serial/tenuredGeneration.cpp
)。ParallelGC:在 ParallelGC 的情況下,
MinHeapFreeRatio
?與?MaxHeapFreeRatio
?指的是整個(gè)堆的大小。并且,如果這兩個(gè) JVM 參數(shù)沒(méi)有明確指定的話,那么?MinHeapFreeRatio
?就是 0,MaxHeapFreeRatio
?就是 100(這里的源碼參考:https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/gc/parallel/parallelArguments.cpp
),相當(dāng)于不會(huì)根據(jù)這兩個(gè)參數(shù)調(diào)整堆大小。并且,如果?UseAdaptiveSizePolicy
?是 false 的話,這兩個(gè)參數(shù)也不會(huì)生效。G1GC:在 G1GC 的情況下,
MinHeapFreeRatio
?與?MaxHeapFreeRatio
?指的是整個(gè)堆的大小。在觸發(fā)涉及老年代的 GC 的時(shí)候,GC 結(jié)束時(shí),會(huì)查看當(dāng)前堆的空閑比例,與?MinHeapFreeRatio
?和?MaxHeapFreeRatio
比較判斷是否擴(kuò)容還是縮小堆,通過(guò)增加或者減少 Region 數(shù)量進(jìn)行堆的擴(kuò)容與縮?。ㄟ@里的源碼參考:https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/gc/g1/g1HeapSizingPolicy.cpp
)。ShenandoahGC:這三個(gè)參數(shù)不生效
ZGC:這三個(gè)參數(shù)不生效
3.9. 適用于長(zhǎng)期運(yùn)行并且盡量將所有可用內(nèi)存被堆使用的 JVM 參數(shù) AggressiveHeap
AggressiveHeap
?是一種激進(jìn)地讓 JVM 使用當(dāng)前系統(tǒng)的剩余內(nèi)存的一種配置,開(kāi)啟會(huì)根據(jù)系統(tǒng)可用內(nèi)存,自動(dòng)設(shè)置堆大小等內(nèi)存參數(shù),將內(nèi)存的一半分配給堆,另一半留給堆外其他的子系統(tǒng)占用內(nèi)存,通過(guò)強(qiáng)制使用 ParallelGC 這種不會(huì)占用太多堆外內(nèi)存的 GC 算法這種類似的思路限制堆外內(nèi)存的使用(只能使用這個(gè) GC,你指定其他 GC 的話會(huì)啟動(dòng)報(bào)錯(cuò)?Error occurred during initialization of VM. Multiple garbage collectors selected
)。默認(rèn)為 false 即不開(kāi)啟,可以通過(guò)?-XX:+AggressiveHeap
?開(kāi)啟。
開(kāi)啟后,首先檢查系統(tǒng)內(nèi)存大小是否足夠 256 MB,如果不夠會(huì)報(bào)錯(cuò),夠得話,會(huì)計(jì)算出一個(gè)目標(biāo)堆大小:
目標(biāo)堆大小 = Math.min(系統(tǒng)可用內(nèi)存/2, 系統(tǒng)可用內(nèi)存 - 160MB)
之后,開(kāi)啟這個(gè)參數(shù)會(huì)強(qiáng)制設(shè)置以下參數(shù):
MaxHeapSize
?最大堆內(nèi)存為目標(biāo)堆大小InitialHeapSize
?初始堆內(nèi)存為目標(biāo)堆大小NewSize
?和?MaxNewSize
?新生代為目標(biāo)堆大小 * 八分之三BaseFootPrintEstimate
?堆外內(nèi)存占用大小預(yù)估為目標(biāo)堆大小,用于指導(dǎo)一些堆外內(nèi)存結(jié)構(gòu)的初始化UseLargePages
?為開(kāi)啟,使用大頁(yè)內(nèi)存分配,增加實(shí)際物理內(nèi)存的連續(xù)性TLABSize
?為 256K,即初始 TLAB 大小為 256 K,但是下面我們?cè)O(shè)置了?ResizeTLAB
?為 false,所以 TLAB 就會(huì)保持為 256KResizeTLAB
?為 false,也就是 TLAB 大小不再隨著 GC 以及分配特征的改變而改變,減少?zèng)]必要的計(jì)算,反正進(jìn)程要長(zhǎng)期存在了,就在初始就指定一個(gè)比較大的 TLAB 的值。如果對(duì) TLAB 細(xì)節(jié)感興趣,請(qǐng)參考系列的第一部:全網(wǎng)最硬核 JVM TLAB 解析UseParallelGC
?為 true,強(qiáng)制使用 ParallelGCThresholdTolerance
?為最大值 100,ThresholdTolerance
?用于動(dòng)態(tài)控制對(duì)象晉升到老年代需要存活過(guò)的 GC 次數(shù),如果?1 + ThresholdTolerance/100
?* MinorGC 時(shí)間大于 MajorGC 的時(shí)間,我們就認(rèn)為 MinorGC 占比過(guò)大,需要將更多對(duì)象晉升到老年代。反之,如果?1 + ThresholdTolerance/100
?* MajorGC 時(shí)間大于 MinorGC 的時(shí)間,就認(rèn)為 MajorGC 時(shí)間占比過(guò)多,需要將更少的對(duì)象晉升到老年代。調(diào)整成 100 可以實(shí)現(xiàn)這個(gè)晉升界限基本不變保持穩(wěn)定。ScavengeBeforeFullGC
?設(shè)置為 false,在 FullGC 之前,先嘗試執(zhí)行一次 YoungGC。因?yàn)殚L(zhǎng)期運(yùn)行的應(yīng)用,會(huì)經(jīng)常 YoungGC 并晉升對(duì)象,需要 FullGC 的時(shí)候一般 YoungGC 無(wú)法回收那么多內(nèi)存避免 FullGC,關(guān)閉它更有利于避免無(wú)效掃描弄臟 CPU 緩存。
3.10. JVM 參數(shù) AlwaysPreTouch 的作用
在第二章的分析中,我們知道了 JVM 申請(qǐng)內(nèi)存的流程,內(nèi)存并不是在 JVM commit 一塊內(nèi)存之后就立刻被操作系統(tǒng)分配實(shí)際的物理內(nèi)存的,只有真正往里面寫(xiě)數(shù)據(jù)的時(shí)候,才會(huì)關(guān)聯(lián)實(shí)際的物理內(nèi)存。所以對(duì)于 JVM 堆內(nèi)存,我們也可以推測(cè)出,堆內(nèi)存隨著對(duì)象的分配才會(huì)關(guān)聯(lián)實(shí)際的物理內(nèi)存。那我們有沒(méi)有辦法提前強(qiáng)制讓 committed 的內(nèi)存關(guān)聯(lián)實(shí)際的物理內(nèi)存呢?很簡(jiǎn)單,往這些 committed 的內(nèi)存中寫(xiě)入假數(shù)據(jù)就行了(一般是填充 0)。
對(duì)于不同的 GC,由于不同 GC 對(duì)于堆內(nèi)存的設(shè)計(jì)不同,所以對(duì)于 AlwaysPreTouch 的處理也略有不同,在以后的系列我們?cè)敿?xì)解析每一種 GC 的時(shí)候,會(huì)詳細(xì)分析每種 GC 的堆內(nèi)存設(shè)計(jì),這里我們就簡(jiǎn)單列舉通用的 AlwaysPreTouch 處理。AlwaysPreTouch 打開(kāi)后,所有新 commit 的堆內(nèi)存,都會(huì)往里面填充 0,相當(dāng)于寫(xiě)入空數(shù)據(jù)讓 commit 的內(nèi)存真正被分配。
不同操作系統(tǒng)環(huán)境下填充 0 的實(shí)現(xiàn)方式不太一樣,但是基本思路是通過(guò)原子的方式給內(nèi)存地址加 0 實(shí)現(xiàn):https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/runtime/os.cpp
:
void os::pretouch_memory(void* start, void* end, size_t page_size) {
?if (start < end) {
? ?//對(duì)齊起始與末尾
? ?char* cur = static_cast<char*>(align_down(start, page_size));
? ?void* last = align_down(static_cast<char*>(end) - 1, page_size);
? ?//對(duì)內(nèi)存寫(xiě)入空數(shù)據(jù),通過(guò) Atomic::add
? ?for ( ; true; cur += page_size) {
? ? ?Atomic::add(reinterpret_cast<int*>(cur), 0, memory_order_relaxed);
? ? ?if (cur >= last) break;
? ?}
?}
}
在 linux x86 環(huán)境下,Atomic::add
?的實(shí)現(xiàn)是通過(guò)?xaddq
?加?lock
?指令實(shí)現(xiàn):?https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/os_cpu/linux_x86/atomic_linux_x86.hpp
:
template<>
template<typename D, typename I>
inline D Atomic::PlatformAdd<8>::fetch_and_add(D volatile* dest, I add_value,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? atomic_memory_order order) const {
?STATIC_ASSERT(8 == sizeof(I));
?STATIC_ASSERT(8 == sizeof(D));
?D old_value;
?__asm__ __volatile__ ("lock xaddq %0,(%2)"
? ? ? ? ? ? ? ? ? ? ? ?: "=r" (old_value)
? ? ? ? ? ? ? ? ? ? ? ?: "0" (add_value), "r" (dest)
? ? ? ? ? ? ? ? ? ? ? ?: "cc", "memory");
?return old_value;
}
同時(shí),如果只是串行地處理這些?Atomic::add
,那是非常非常慢的。我們可以將要 preTouch 的內(nèi)存分成不相交的區(qū)域,然后并發(fā)的填充這些不相交的內(nèi)存區(qū)域,目前最新版本的 Java 都已經(jīng)在各種不同的并發(fā) GC 中實(shí)現(xiàn)了并發(fā)的 PreTouch,但是歷史上不同 GC 出現(xiàn)過(guò)對(duì)于 AlwaysPreTouch 的不同問(wèn)題,這里匯總下(Plagiarism真的可惡,滾開(kāi)好么):
ParallelGC:
Bug:
https://bugs.openjdk.org/browse/JDK-8252221
Commit:
https://github.com/openjdk/jdk/commit/9359ff03ae6b9e09e7defef148864f40e949b669
從 Java 16 build 21 開(kāi)始,ParallelGC 才實(shí)現(xiàn)并發(fā) PreTouch:
G1GC:
Bug:
https://bugs.openjdk.org/browse/JDK-8157952
Commit:
https://github.com/openjdk/jdk/commit/317f1aa044a8a71c52cfe733f1f4baf656c22c4c
Bug:
https://bugs.openjdk.org/browse/JDK-8067469
Commit:
https://github.com/openjdk/jdk/commit/f2e110fe7793b20a21f91e8ef7451814db2c8d73
在 Java 9 build 45 之前,AlwaysPreTouch 對(duì)于 G1GC 不生效,這是一個(gè) bug:
從 Java 9 build 139 開(kāi)始,G1GC 才實(shí)現(xiàn)并發(fā) PreTouch:
ZGC:
Bug:
https://bugs.openjdk.org/browse/JDK-8234543
Commit:
https://github.com/openjdk/jdk/commit/5e758d2368b58ceef5092e74d481b60867b5ab93
從 Java 14 build 26 開(kāi)始,ZGC 才實(shí)現(xiàn)并發(fā) PreTouch:
3.11. JVM 參數(shù) UseContainerSupport - JVM 如何感知到容器內(nèi)存限制
在前面的章節(jié)我們分析了 JVM 自動(dòng)計(jì)算堆大小限制,其中第一步就是 JVM 讀取系統(tǒng)內(nèi)存信息。在容器的環(huán)境下,JVM 也能感知到當(dāng)前是容器環(huán)境,并且讀取對(duì)應(yīng)的內(nèi)存限制。讓 JVM 感知容器環(huán)境的相關(guān) JVM 參數(shù)是?UseContainerSupport
,默認(rèn)值為 true,即讓 JVM 感知容器的配置,相關(guān)源碼:https://github.com/openjdk/jdk/blob/jdk-21+3/src/hotspot/os/linux/globals_linux.hpp
:
product(bool, UseContainerSupport, true, ? ? ? ? ? ? ? ? ? ? ? ? ?\
?"Enable detection and runtime container configuration support") \
這個(gè)配置默認(rèn)開(kāi)啟,在開(kāi)啟的情況下,JVM 會(huì)通過(guò)下面的流程讀取內(nèi)存限制:

可以看出,針對(duì) Cgroup V1 與 V2 的情況,以及沒(méi)有限制 pod 的 Memory limit 的情況,都考慮到了。
3.12. SoftMaxHeapSize - 用于平滑遷移更耗內(nèi)存的 GC 使用
由于那種完并發(fā)的 GC(目標(biāo)是完全無(wú) Stop the world 暫?;蛘呤莵喓撩霑和5?GC),例如 ZGC ,需要在堆外使用比 G1GC 以及 ParallelGC 多的多的空間(指的就是我們后面會(huì)分析到的 Native Memory Tracking 的 GC 部分占用的內(nèi)存),并且由于 ZGC 這種目前是未分代的(Java 20 之后會(huì)引入分代 ZGC),導(dǎo)致 GC 在堆外占用的內(nèi)存會(huì)更多。所以我們一般認(rèn)為,在從 G1GC,或者 ParallelGC 切換到 ZGC 的時(shí)候,就算最大堆大小等各種 JVM 參數(shù)不變,JVM 也會(huì)需要更多的物理內(nèi)存。但是,在實(shí)際的生產(chǎn)中,修改 JVM GC 是比較簡(jiǎn)單的,修改下啟動(dòng)參數(shù)就行了,但是給 JVM 加內(nèi)存是比較困難的,因?yàn)槭菍?shí)際要消耗的資源。如果不修改 JVM 內(nèi)存限制參數(shù),也不加可用內(nèi)存,線上可能會(huì)在換 GC 后經(jīng)常出現(xiàn)被 OOMkiller 干掉的情況,還有剽竊狗被干掉了。
為了能讓大家更平滑的切換 GC,以及對(duì)于線上應(yīng)用,我們可能實(shí)際不一定需要用原來(lái)配置的堆大小的空間,JVM 針對(duì) ShenandoahGC 以及 ZGC 引入了 SoftMaxHeapSize 這個(gè)參數(shù)(目前這個(gè)參數(shù)只對(duì)于這種專注于避免全局暫停的 GC 生效)。這個(gè)參數(shù)雖然默認(rèn)是 0,但是如果沒(méi)有指定的話,會(huì)自動(dòng)設(shè)置為前文提到的 MaxHeapSize 大小。參考源碼:
https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/gc/shared/gc_globals.hpp
product(size_t, SoftMaxHeapSize, 0, MANAGEABLE, ? ? ? ? ? ? ? ? ? ? \
?"Soft limit for maximum heap size (in bytes)") ? ? ? ? ? ? ? ? ? ?\
?constraint(SoftMaxHeapSizeConstraintFunc,AfterMemoryInit) ? ? ? ? \
https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/share/gc/shared/gcArguments.cpp
//如果沒(méi)有設(shè)置 SoftMaxHeapSize,自動(dòng)設(shè)置為前文提到的 MaxHeapSize 大小
if (FLAG_IS_DEFAULT(SoftMaxHeapSize)) {
? ?FLAG_SET_ERGO(SoftMaxHeapSize, MaxHeapSize);
}
ZGC 與 ShenandoahGC 的堆設(shè)計(jì),都有軟最大大小限制的概念。這個(gè)軟最大大小是隨著時(shí)間與 GC 表現(xiàn)(例如分配速率,空閑率等)不斷變化的,這兩個(gè) GC 會(huì)在堆擴(kuò)展到軟最大大小之后,盡量就不擴(kuò)展堆大小,盡量通過(guò)激進(jìn)的 GC 回收空間。只有在暫停世界都完全無(wú)法回收足夠內(nèi)存用以分配的時(shí)候,才會(huì)嘗試擴(kuò)展,這之后最大限制就到了 MaxHeapSize。SoftMaxHeapSize 會(huì)給這個(gè)軟最大大小一個(gè)指導(dǎo)值,讓軟最大大小不要超過(guò)這個(gè)值。
微信搜索“干貨滿滿張哈希”關(guān)注公眾號(hào),加作者微信,每日一刷,輕松提升技術(shù),斬獲各種offer
我會(huì)經(jīng)常發(fā)一些很好的各種框架的官方社區(qū)的新聞視頻資料并加上個(gè)人翻譯字幕到如下地址(也包括上面的公眾號(hào)),歡迎關(guān)注:
知乎:https://www.zhihu.com/people/zhxhash