全網(wǎng)最硬核 JVM 內(nèi)存解析 - 2.JVM 內(nèi)存申請(qǐng)與使用流程

個(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ì)象指針與壓縮類(lèi)指針的關(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
首先類(lèi)加載器 1 需要分配 1023 字節(jié)大小的內(nèi)存,屬于類(lèi)空間
然后類(lèi)加載器 1 還需要分配 1023 字節(jié)大小的內(nèi)存,屬于類(lèi)空間
然后類(lèi)加載器 1 需要分配 264 KB 大小的內(nèi)存,屬于類(lèi)空間
然后類(lèi)加載器 1 需要分配 2 MB 大小的內(nèi)存,屬于類(lèi)空間
然后類(lèi)加載器 1 需要分配 128KB 大小的內(nèi)存,屬于類(lèi)空間
新來(lái)一個(gè)類(lèi)加載器 2,需要分配 1023 Bytes 大小的內(nèi)存,屬于類(lèi)空間
然后類(lèi)加載器 1 被 GC 回收掉
然后類(lèi)加載器 2 需要分配 1 MB 大小的內(nèi)存,屬于類(lè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
類(lèi)加載的入口?
SystemDictionary
?與保留所有?ClassLoaderData
?的?ClassLoaderDataGraph
每個(gè)類(lèi)加載器私有的?
ClassLoaderData
?以及?ClassLoaderMetaspace
管理正在使用的?
MetaChunk
?的?MetaspaceArena
元空間內(nèi)存分配流程(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 9.元空間內(nèi)存分配流程開(kāi)始)
ClassLoaderData
?回收ChunkHeaderPool
?池化?MetaChunk
?對(duì)象ChunkManager
?管理空閑的?MetaChunk
類(lèi)加載器到?
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
2. JVM 內(nèi)存申請(qǐng)與使用流程
2.1. Linux 下內(nèi)存管理模型簡(jiǎn)述
Linux 內(nèi)存管理模型不是咱們這個(gè)系列的討論重點(diǎn),我們這里只會(huì)簡(jiǎn)單提一些對(duì)于咱們這個(gè)系列需要了解到的,如果讀者想要深入理解,建議大家查看?bin 神(公眾號(hào):bin 的技術(shù)小屋)的系列文章:一步一圖帶你深入理解 Linux 虛擬內(nèi)存管理
CPU 是通過(guò)尋址來(lái)訪問(wèn)內(nèi)存的,目前大部分 CPU 都是 64 位的,即尋址范圍是:0x0000 0000 0000 0000 ~ 0xFFFF FFFF FFFF FFFF
,即可以管理 16EB 的內(nèi)存。但是,實(shí)際程序并不會(huì)直接通過(guò) CPU 尋址訪問(wèn)到實(shí)際的物理內(nèi)存,而是通過(guò)引入 MMU(Memory Management Unit 內(nèi)存管理單元)與實(shí)際物理地址隔了一層虛擬內(nèi)存的抽象。這樣,程序申請(qǐng)以及訪問(wèn)的其實(shí)是虛擬內(nèi)存地址,MMU 會(huì)將這個(gè)虛擬內(nèi)存地址映射為實(shí)際的物理內(nèi)存地址。同時(shí),為了減少內(nèi)存碎片,以及增加內(nèi)存分配效率,在 MMU 的基礎(chǔ)上 Linux 抽象了內(nèi)存分頁(yè)(Paging)的概念,將虛擬地址按固定大小分割成頁(yè)(默認(rèn)是 4K,如果平臺(tái)支持更多更大的頁(yè)大小 JVM 也是可以利用的,我們后面分析相關(guān)的 JVM 參數(shù)會(huì)看到),并在頁(yè)被實(shí)際使用寫(xiě)入數(shù)據(jù)的時(shí)候,映射同樣大小的實(shí)際的物理內(nèi)存(頁(yè)幀,Page Frame),或者是在物理內(nèi)存不足的時(shí)候,將某些不常用的頁(yè)轉(zhuǎn)移到其他存儲(chǔ)設(shè)備比如磁盤(pán)上。
一般系統(tǒng)中會(huì)有多個(gè)進(jìn)程使用內(nèi)存,每個(gè)進(jìn)程都有自己獨(dú)立的虛擬內(nèi)存空間,假設(shè)我們這里有三個(gè)進(jìn)程,進(jìn)程 A 訪問(wèn)的虛擬地址可以與進(jìn)程 B 和進(jìn)程 C 的虛擬地址相同,那么操作系統(tǒng)如何區(qū)分呢?即操作系統(tǒng)如何將這些虛擬地址轉(zhuǎn)換為物理內(nèi)存。這就需要頁(yè)表了,頁(yè)表也是每個(gè)進(jìn)程獨(dú)立的,操作系統(tǒng)會(huì)在給進(jìn)程映射物理內(nèi)存用來(lái)保存用戶數(shù)據(jù)的時(shí)候,將物理內(nèi)存保存到進(jìn)程的頁(yè)表里面。然后,進(jìn)程訪問(wèn)虛擬內(nèi)存空間的時(shí)候,通過(guò)頁(yè)表找到物理內(nèi)存:

頁(yè)表如何將一個(gè)虛擬內(nèi)存地址(我們需要注意一點(diǎn),目前虛擬內(nèi)存地址,用戶空間與內(nèi)核空間可以使用從?0x0000 0000 0000 0000 ~ 0x0000 FFFF FFFF FFFF
?的地址,即 256TB),轉(zhuǎn)化為物理內(nèi)存的呢?下面我們舉一個(gè)在 x86,64 位環(huán)境下四級(jí)頁(yè)表的結(jié)構(gòu)視圖:

在這里,頁(yè)表分為四個(gè)級(jí)別:PGD(Page Global Directory),PUD(Page Upper Directory),PMD(Page Middle Directory),PTE(Page Table Entry)。每個(gè)頁(yè)表,里面的頁(yè)表項(xiàng),保存了指向下一個(gè)級(jí)別的頁(yè)表的引用,除了最后一層的 PTE 里面的頁(yè)表項(xiàng)保存的是指向用戶數(shù)據(jù)內(nèi)存的指針。如何將虛擬內(nèi)存地址通過(guò)頁(yè)表找到對(duì)應(yīng)用戶數(shù)據(jù)內(nèi)存從而讀取數(shù)據(jù),過(guò)程是:

取虛擬地址的?
39 ~ 47
?位(因?yàn)橛脩艨臻g與內(nèi)核空間可以使用從?0x0000 0000 0000 0000 ~ 0x0000 FFFF FFFF FFFF
?的地址,即 47 位以下的地址)作為 offset,在唯一的 PGD 頁(yè)面根據(jù) offset 定位到 PGD 頁(yè)表項(xiàng)?pgd_t
使用?
pgd_t
?定位到具體的 PUD 頁(yè)面取虛擬地址的?
30 ~ 38
?位作為 offset,在對(duì)應(yīng)的 PUD 頁(yè)面根據(jù) offset 定位到 PUD 頁(yè)表項(xiàng)?pud_t
使用?
pud_t
?定位到具體的 PMD 頁(yè)面取虛擬地址的?
21 ~ 29
?位作為 offset,在對(duì)應(yīng)的 PMD 頁(yè)面根據(jù) offset 定位到 PMD 頁(yè)表項(xiàng)?pmd_t
使用?
pmd_t
?定位到具體的 PTE 頁(yè)面取虛擬地址的?
12 ~ 20
?位作為 offset,在對(duì)應(yīng)的 PTE 頁(yè)面根據(jù) offset 定位到 PTE 頁(yè)表項(xiàng)?pte_t
使用?
pte_t
?定位到具體的用戶數(shù)據(jù)物理內(nèi)存頁(yè)面使用最后的?
0 ~ 11
?位作為 offset,對(duì)應(yīng)到用戶數(shù)據(jù)物理內(nèi)存頁(yè)面的對(duì)應(yīng) offset
如果每次訪問(wèn)虛擬內(nèi)存,都需要訪問(wèn)這個(gè)頁(yè)表翻譯成實(shí)際物理內(nèi)存的話,性能太差。所以一般 CPU 里面都有一個(gè) TLB(Translation Lookaside Buffer,翻譯后備緩沖)存在,一般它屬于 CPU 的 MMU 的一部分。TLB 負(fù)責(zé)緩存虛擬內(nèi)存與實(shí)際物理內(nèi)存的映射關(guān)系,一般 TLB 容量很小。每次訪問(wèn)虛擬內(nèi)存,先查看 TLB 中是否有緩存,如果沒(méi)有才會(huì)去頁(yè)表查詢。

默認(rèn)情況下,TLB 緩存的 key 為地址的?12 ~ 47
?位,value 是實(shí)際的物理內(nèi)存頁(yè)面。這樣前面從第 1 到第 7 步就可以被替換成訪問(wèn) TLB 了:
取虛擬地址的?
12 ~ 47
?位作為 key,訪問(wèn) TLB,定位到具體的用戶數(shù)據(jù)物理內(nèi)存頁(yè)面。使用最后的?
0 ~ 11
?位作為 offset,對(duì)應(yīng)到用戶數(shù)據(jù)物理內(nèi)存頁(yè)面的對(duì)應(yīng) offset。

TLB 一般很小,我們來(lái)看幾個(gè) CPU 中的 TLB 大小,以下圖片來(lái)自于?https://www.bilibili.com/video/BV1Xx4y1j7Hu/?spm_id_from=333.999.0.0

我們這里不用關(guān)心 iTLB,dTLB,sTLB 分別是什么意思,只要可以看出兩點(diǎn)即可:1. TLB 整體可以容納個(gè)數(shù)不多;2. 頁(yè)大小越大,TLB 能容納的個(gè)數(shù)越少。但是整體看,TLB 能容納的頁(yè)大小還是增多的(比如 Nehalem 的 iTLB,頁(yè)大小 4K 的時(shí)候,一共可以容納?128 * 4 = 512K
?的內(nèi)存,頁(yè)大小 2M 的時(shí)候,一共可以容納?2 * 7 = 14M
?的內(nèi)存)。
JVM 中很多地方需要知道頁(yè)大小,JVM 在初始化的時(shí)候,通過(guò)系統(tǒng)調(diào)用?sysconf(_SC_PAGESIZE)
?讀取出頁(yè)大小,并保存下來(lái)以供后續(xù)使用。參考源碼:https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/os/linux/os_linux.cpp
:
????//設(shè)置全局默認(rèn)頁(yè)大小,通過(guò) Linux::page_size() 可以獲取全局默認(rèn)頁(yè)大小
????Linux::set_page_size(sysconf(_SC_PAGESIZE));
????if?(Linux::page_size() ==?-1) {
????????fatal("os_linux.cpp: os::init: sysconf failed (%s)",
??????????os::strerror(errno));
????}
????//將默認(rèn)頁(yè)大小加入可選的頁(yè)大小列表,在涉及大頁(yè)分配的時(shí)候有用
????_page_sizes.add(Linux::page_size());
2.2. JVM 主要內(nèi)存申請(qǐng)分配流程
第一步,JVM 的每個(gè)子系統(tǒng)(例如 Java 堆,元空間,JIT 代碼緩存,GC 等等等等),如果需要的話,在初始化的時(shí)候首先 Reserve 要分配區(qū)域的最大限制大小的內(nèi)存(這個(gè)最大大小,需要按照頁(yè)大小對(duì)齊(即是頁(yè)大小的整數(shù)倍),默認(rèn)頁(yè)大小是前面提到的?Linux::page_size()
),例如對(duì)于 Java 堆,就是最大堆大小(通過(guò)?-Xmx
?或者?-XX:MaxHeapSize
限制),還有對(duì)于代碼緩存,也是最大代碼緩存大?。ㄍㄟ^(guò)?-XX:ReservedCodeCacheSize
?限制)。Reserve 的目的是在虛擬內(nèi)存空間劃出一塊內(nèi)存專(zhuān)門(mén)給某個(gè)區(qū)域使用,這樣做的好處是:
隔離每個(gè) JVM 子系統(tǒng)使用的內(nèi)存的虛擬空間,這樣在 JVM 代碼有 bug 的時(shí)候(例如發(fā)生 Segment Fault 異常),通過(guò)報(bào)錯(cuò)中的虛擬內(nèi)存地址可以快速定位到是哪個(gè)子系統(tǒng)出了問(wèn)題。
可以很方便的限制這個(gè)區(qū)域使用的最大內(nèi)存大小。
便于管理,Reserve 不會(huì)觸發(fā)操作系統(tǒng)分配映射實(shí)際物理內(nèi)存,這個(gè)區(qū)域可以在 Reserve 的區(qū)域內(nèi)按需伸縮。
便于一些 JIT 優(yōu)化,例如我們故意將這個(gè)區(qū)域保留起來(lái)但是故意不將這個(gè)區(qū)域的虛擬內(nèi)存映射物理內(nèi)存,訪問(wèn)這塊內(nèi)存會(huì)造成 Segment Fault 異常。JVM 會(huì)預(yù)設(shè) Segment Fault 異常的處理器,在處理器里面檢查發(fā)生 Segment Fault 異常的內(nèi)存地址屬于哪個(gè)子系統(tǒng)的 Reserve 的區(qū)域,判斷要做什么操作。后面我們會(huì)看到,null 檢查拋出?
NullPointerException
?異常的優(yōu)化,全局安全點(diǎn),拋出?StackOverflowError
?的實(shí)現(xiàn),都和這個(gè)機(jī)制有關(guān)。
在 Linux 的環(huán)境下,Reserve 通過(guò)?mmap(2)
?系統(tǒng)調(diào)用實(shí)現(xiàn),參數(shù)傳入?prot = PROT_NONE
,PROT_NONE
?代表不會(huì)使用,即不能做任何操作,包括讀和寫(xiě)。為啥要打擊抄襲,稿主被抄襲太多所以斷更很久。如果 JVM 使用這塊內(nèi)存,會(huì)發(fā)生 Segment Fault 異常。Reserve 的源碼,對(duì)應(yīng)的是:
入口為:https://github.com/openjdk/jdk/blob/jdk-21+9/src/hotspot/share/runtime/os.cpp
char* os::reserve_memory(size_t bytes, bool executable, MEMFLAGS flags) { ? //調(diào)用每個(gè)操作系統(tǒng)實(shí)現(xiàn)不同的 pd_reserve_memory 函數(shù)進(jìn)行 reserve ? char* result = pd_reserve_memory(bytes, executable); ? if (result != NULL) { ? ? MemTracker::record_virtual_memory_reserve(result, bytes, CALLER_PC, flags); ? }不要偷取他人的勞動(dòng)成果,也不要浪費(fèi)自己的時(shí)間和精力,讓我們一起做一個(gè)有良知的寫(xiě)作者。 ? return result; }
對(duì)應(yīng) linux 的實(shí)現(xiàn)是:https://github.com/openjdk/jdk/blob/jdk-21+9/src/hotspot/os/linux/os_linux.cpp
char* os::pd_reserve_memory(size_t bytes, bool exec) { ? return anon_mmap(nullptr, bytes); } static char* anon_mmap(char* requested_addr, size_t bytes) { ? const int flags = MAP_PRIVATE | MAP_NORESERVE | MAP_ANONYMOUS; ? //這里的關(guān)鍵是 PROT_NONE,代表僅僅是在虛擬空間保留,不實(shí)際映射物理內(nèi)存 ? //fd 傳入的是 -1,因?yàn)闆](méi)有實(shí)際映射文件,我們這里目的是為了分配內(nèi)存,不是將某個(gè)文件映射到內(nèi)存中 ? char* addr = (char*)::mmap(requested_addr, bytes, PROT_NONE, flags, -1, 0); ? return addr == MAP_FAILED ? NULL : addr; }
第二步,JVM 的每個(gè)子系統(tǒng),按照各自的策略,通過(guò) Commit 第一步 Reserve 的區(qū)域的一部分?jǐn)U展內(nèi)存(大小也一般頁(yè)大小對(duì)齊的),從而向操作系統(tǒng)申請(qǐng)映射物理內(nèi)存,通過(guò) Uncommit 已經(jīng) Commit 的內(nèi)存來(lái)釋放物理內(nèi)存給操作系統(tǒng)。抄襲和xigao是文化的毒瘤,是對(duì)文化創(chuàng)造和發(fā)展的阻礙!
Commit 的源碼入口為:https://github.com/openjdk/jdk/blob/jdk-21+9/src/hotspot/share/runtime/os.cpp
bool os::commit_memory(char* addr, size_t bytes, bool executable) { ? assert_nonempty_range(addr, bytes); ? //調(diào)用每個(gè)操作系統(tǒng)實(shí)現(xiàn)不同的 pd_commit_memory 函數(shù)進(jìn)行 commit ? bool res = pd_commit_memory(addr, bytes, executable); ? if (res) { ? ? MemTracker::record_virtual_memory_commit((address)addr, bytes, CALLER_PC); ? } ? return res; }
對(duì)應(yīng) linux 的實(shí)現(xiàn)是:https://github.com/openjdk/jdk/blob/jdk-21+9/src/hotspot/os/linux/os_linux.cpp
bool os::pd_commit_memory(char* addr, size_t size, bool exec) { ? return os::Linux::commit_memory_impl(addr, size, exec) == 0; } int os::Linux::commit_memory_impl(char* addr, size_t size, bool exec) { ? //這里的關(guān)鍵是 PROT_READ|PROT_WRITE,即申請(qǐng)需要讀寫(xiě)這塊內(nèi)存 ? int prot = exec ? PROT_READ|PROT_WRITE|PROT_EXEC : PROT_READ|PROT_WRITE; ? uintptr_t res = (uintptr_t) ::mmap(addr, size, prot, ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0); ? if (res != (uintptr_t) MAP_FAILED) { ? ? if (UseNUMAInterleaving) { ? ? ? numa_make_global(addr, size); ? ? } ? ? return 0; ? } ? int err = errno; ?// save errno from mmap() call above ? if (!recoverable_mmap_error(err)) { ? ? warn_fail_commit_memory(addr, size, exec, err); ? ? vm_exit_out_of_memory(size, OOM_MMAP_ERROR, "committing reserved memory."); ? } ? return err; }
Commit 內(nèi)存之后,并不是操作系統(tǒng)會(huì)立刻分配物理內(nèi)存,而是在向 Commit 的內(nèi)存里面寫(xiě)入數(shù)據(jù)的時(shí)候,操作系統(tǒng)才會(huì)實(shí)際映射內(nèi)存。plagiarism和洗稿是惡意抄襲他人勞動(dòng)成果的行為,是對(duì)勞動(dòng)價(jià)值的漠視和踐踏!JVM 有對(duì)應(yīng)的參數(shù),可以在 Commit 內(nèi)存后立刻寫(xiě)入 0 來(lái)強(qiáng)制操作系統(tǒng)分配內(nèi)存,即?AlwaysPreTouch
?這個(gè)參數(shù),這個(gè)參數(shù)我們后面會(huì)詳細(xì)分析以及歷史版本存在的缺陷。
我們?cè)賮?lái)看下為什么先 Reserve 之后 Commit 這樣好 Debug??催@個(gè)例子,如果我們沒(méi)有第一步 Reserve,直接是第二步 Commit,那么我們可能分配的內(nèi)存是這個(gè)樣子的:

假設(shè)此時(shí),我們不小心在 JVM 中寫(xiě)了個(gè) bug,導(dǎo)致洗稿狗沒(méi)了媽?zhuān)瑢?dǎo)致 MetaSpace 2 這塊內(nèi)存被回收了,這時(shí)候保留指向 MetaSpace 2 的內(nèi)存的指針就會(huì)報(bào) Segment Fault,但是通過(guò) Segment Fault 里面帶的地址,我們并不知道是這個(gè)地址屬于哪里,除非我們有另外的內(nèi)存結(jié)構(gòu)保存每個(gè)子系統(tǒng) Commit 內(nèi)存的列表,但是這樣效率太低了。如果我們先 Reserve 大塊之后在里面 Commit,那么情況就不同了:

這樣,只需要判斷 Segment Fault 里面帶的地址處于的范圍,就能知道是哪個(gè)子系統(tǒng)
2.2.1. JVM commit 的內(nèi)存與實(shí)際占用內(nèi)存的差異
前面一節(jié)我們知道了,JVM 中大塊內(nèi)存,基本都是先 reserve 一大塊,之后 commit 其中需要的一小塊,然后開(kāi)始讀寫(xiě)處理內(nèi)存,在 Linux 環(huán)境下,底層基于?mmap(2)
?實(shí)現(xiàn)。但是需要注意一點(diǎn)的是,commit 之后,內(nèi)存并不是立刻被分配了物理內(nèi)存,而是真正往內(nèi)存中 store 東西的時(shí)候,才會(huì)真正映射物理內(nèi)存,如果是 load 讀取也是可能不映射物理內(nèi)存的。
這其實(shí)是可能你平??吹降呛雎缘默F(xiàn)象,如果你使用的是 SerialGC,ParallelGC 或者 CMS GC,老年代的內(nèi)存在有對(duì)象晉升到老年代之前,可能是不會(huì)映射物理內(nèi)存的,雖然這塊內(nèi)存已經(jīng)被 commit 了。并且年輕代可能也是隨著使用才會(huì)映射物理內(nèi)存。如果你用的是 ZGC,G1GC,或者 ShenandoahGC,那么內(nèi)存用的會(huì)更激進(jìn)些(主要因?yàn)榉謪^(qū)算法劃分導(dǎo)致內(nèi)存被寫(xiě)入),這是你在換 GC 之后看到物理內(nèi)存內(nèi)存快速上漲的原因之一。JVM 有對(duì)應(yīng)的參數(shù),可以在 Commit 內(nèi)存后立刻寫(xiě)入 0 來(lái)強(qiáng)制操作系統(tǒng)分配內(nèi)存,即?AlwaysPreTouch
?這個(gè)參數(shù),這個(gè)參數(shù)我們后面會(huì)詳細(xì)分析以及歷史版本存在的缺陷。還有的差異,主要來(lái)源于在 uncommit 之后,系統(tǒng)可能還沒(méi)有來(lái)的及將這塊物理內(nèi)存真正回收。
所以,JVM 認(rèn)為自己 commit 的內(nèi)存,與實(shí)際系統(tǒng)分配的物理內(nèi)存,可能是有差異的,可能 JVM 認(rèn)為自己 commit 的內(nèi)存比系統(tǒng)分配的物理內(nèi)存多,也可能少。這就是為啥 Native Memory Tracking(JVM 認(rèn)為自己 commit 的內(nèi)存)與實(shí)際其他系統(tǒng)監(jiān)控中體現(xiàn)的物理內(nèi)存使用指標(biāo)對(duì)不上的原因。
微信搜索“干貨滿滿張哈?!标P(guān)注公眾號(hào),加作者微信,每日一刷,輕松提升技術(shù),斬獲各種offer
我會(huì)經(jīng)常發(fā)一些很好的各種框架的官方社區(qū)的新聞視頻資料并加上個(gè)人翻譯字幕到如下地址(也包括上面的公眾號(hào)),歡迎關(guān)注:
知乎:https://www.zhihu.com/people/zhxhash