最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

全網(wǎng)最硬核 JVM 內(nèi)存解析 - 5.壓縮對象指針相關(guān)機(jī)制

2023-04-26 23:13 作者:干貨滿滿張哈希  | 我要投稿

個(gè)人創(chuàng)作公約:本人聲明創(chuàng)作的所有文章皆為自己原創(chuàng),如果有參考任何文章的地方,會標(biāo)注出來,如果有疏漏,歡迎大家批判。如果大家發(fā)現(xiàn)網(wǎng)上有抄襲本文章的,歡迎舉報(bào),并且積極向這個(gè)?github 倉庫?提交 issue,謝謝支持~
另外,本文為了避免抄襲,會在不影響閱讀的情況下,在文章的隨機(jī)位置放入對于抄襲和洗稿的人的“親切”的問候。如果是正常讀者看到,筆者在這里說聲對不起,。如果被抄襲狗或者洗稿狗看到了,希望你能夠好好反思,不要再抄襲了,謝謝。
今天又是干貨滿滿的一天,這是全網(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í)候人們一些部分說的是 JVM Specification,一部分說的是 Hotspot 實(shí)現(xiàn),給人一種割裂感與誤解。本篇主要從 Hotspot 實(shí)現(xiàn)出發(fā),以 Linux x86 環(huán)境為主,緊密貼合 JVM 源碼并且輔以各種 JVM 工具驗(yàn)證幫助大家理解 JVM 內(nèi)存的結(jié)構(gòu)。但是,本篇僅限于對于這些內(nèi)存的用途,使用限制,相關(guān)參數(shù)的分析,有些地方可能比較深入,有些地方可能需要結(jié)合本身用這塊內(nèi)存涉及的 JVM 模塊去說,會放在另一系列文章詳細(xì)描述。最后,洗稿抄襲狗不得 house

本篇全篇目錄(以及涉及的 JVM 參數(shù)):

  1. 從 Native Memory Tracking 說起(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 1.從 Native Memory Tracking 說起開始)

    1. Native Memory Tracking 的開啟

    2. Native Memory Tracking 的使用(涉及 JVM 參數(shù):NativeMemoryTracking

    3. Native Memory Tracking 的 summary 信息每部分含義

    4. Native Memory Tracking 的 summary 信息的持續(xù)監(jiān)控

    5. 為何 Native Memory Tracking 中申請的內(nèi)存分為 reserved 和 committed

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

    1. Linux 大頁分配方式 - Huge Translation Lookaside Buffer Page (hugetlbfs)

    2. Linux 大頁分配方式 - Transparent Huge Pages (THP)

    3. JVM 大頁分配相關(guān)參數(shù)與機(jī)制(涉及 JVM 參數(shù):UseLargePages,UseHugeTLBFS,UseSHM,UseTransparentHugePages,LargePageSizeInBytes

    4. JVM commit 的內(nèi)存與實(shí)際占用內(nèi)存的差異

    5. Linux 下內(nèi)存管理模型簡述

    6. JVM commit 的內(nèi)存與實(shí)際占用內(nèi)存的差異

    7. 大頁分配 UseLargePages(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 3.大頁分配 UseLargePages開始)

  3. Java 堆內(nèi)存相關(guān)設(shè)計(jì)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 4.Java 堆內(nèi)存大小的確認(rèn)開始)

    1. 驗(yàn)證?32-bit?壓縮指針模式

    2. 驗(yàn)證?Zero based?壓縮指針模式

    3. 驗(yàn)證?Non-zero disjoint?壓縮指針模式

    4. 驗(yàn)證?Non-zero based?壓縮指針模式

    5. 壓縮對象指針存在的意義(涉及 JVM 參數(shù):ObjectAlignmentInBytes

    6. 壓縮對象指針與壓縮類指針的關(guān)系演進(jìn)(涉及 JVM 參數(shù):UseCompressedOops,UseCompressedClassPointers

    7. 壓縮對象指針的不同模式與尋址優(yōu)化機(jī)制(涉及 JVM 參數(shù):ObjectAlignmentInBytes,HeapBaseMinAddress

    8. 通用初始化與擴(kuò)展流程

    9. 直接指定三個(gè)指標(biāo)的方式(涉及 JVM 參數(shù):MaxHeapSize,MinHeapSize,InitialHeapSize,Xmx,Xms

    10. 不手動(dòng)指定三個(gè)指標(biāo)的情況下,這三個(gè)指標(biāo)(MinHeapSize,MaxHeapSize,InitialHeapSize)是如何計(jì)算的

    11. 壓縮對象指針相關(guān)機(jī)制(涉及 JVM 參數(shù):UseCompressedOops)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 5.壓縮對象指針相關(guān)機(jī)制開始)

    12. 為何預(yù)留第 0 頁,壓縮對象指針 null 判斷擦除的實(shí)現(xiàn)(涉及 JVM 參數(shù):HeapBaseMinAddress

    13. 結(jié)合壓縮對象指針與前面提到的堆內(nèi)存限制的初始化的關(guān)系(涉及 JVM 參數(shù):HeapBaseMinAddress,ObjectAlignmentInBytes,MinHeapSize,MaxHeapSize,InitialHeapSize

    14. 使用 jol + jhsdb + JVM 日志查看壓縮對象指針與 Java 堆驗(yàn)證我們前面的結(jié)論

    15. 堆大小的動(dòng)態(tài)伸縮(涉及 JVM 參數(shù):MinHeapFreeRatio,MaxHeapFreeRatio,MinHeapDeltaBytes)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 6.其他 Java 堆內(nèi)存相關(guān)的特殊機(jī)制開始)

    16. 適用于長期運(yùn)行并且盡量將所有可用內(nèi)存被堆使用的 JVM 參數(shù) AggressiveHeap

    17. JVM 參數(shù) AlwaysPreTouch 的作用

    18. JVM 參數(shù) UseContainerSupport - JVM 如何感知到容器內(nèi)存限制

    19. JVM 參數(shù) SoftMaxHeapSize - 用于平滑遷移更耗內(nèi)存的 GC 使用

  4. JVM 元空間設(shè)計(jì)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 7.元空間存儲的元數(shù)據(jù)開始)

    1. jcmd <pid> VM.metaspace?元空間說明

    2. 元空間相關(guān) JVM 日志

    3. 元空間 JFR 事件詳解

    4. jdk.MetaspaceSummary?元空間定時(shí)統(tǒng)計(jì)事件

    5. jdk.MetaspaceAllocationFailure?元空間分配失敗事件

    6. jdk.MetaspaceOOM?元空間 OOM 事件

    7. jdk.MetaspaceGCThreshold?元空間 GC 閾值變化事件

    8. jdk.MetaspaceChunkFreeListSummary?元空間 Chunk FreeList 統(tǒng)計(jì)事件

    9. CommitLimiter?的限制元空間可以 commit 的內(nèi)存大小以及限制元空間占用達(dá)到多少就開始嘗試 GC

    10. 每次 GC 之后,也會嘗試重新計(jì)算?_capacity_until_GC

    11. 首先類加載器 1 需要分配 1023 字節(jié)大小的內(nèi)存,屬于類空間

    12. 然后類加載器 1 還需要分配 1023 字節(jié)大小的內(nèi)存,屬于類空間

    13. 然后類加載器 1 需要分配 264 KB 大小的內(nèi)存,屬于類空間

    14. 然后類加載器 1 需要分配 2 MB 大小的內(nèi)存,屬于類空間

    15. 然后類加載器 1 需要分配 128KB 大小的內(nèi)存,屬于類空間

    16. 新來一個(gè)類加載器 2,需要分配 1023 Bytes 大小的內(nèi)存,屬于類空間

    17. 然后類加載器 1 被 GC 回收掉

    18. 然后類加載器 2 需要分配 1 MB 大小的內(nèi)存,屬于類空間

    19. 元空間的整體配置以及相關(guān)參數(shù)(涉及 JVM 參數(shù):MetaspaceSize,MaxMetaspaceSize,MinMetaspaceExpansion,MaxMetaspaceExpansion,MaxMetaspaceFreeRatio,MinMetaspaceFreeRatio,UseCompressedClassPointers,CompressedClassSpaceSize,CompressedClassSpaceBaseAddress,MetaspaceReclaimPolicy

    20. 元空間上下文?MetaspaceContext

    21. 虛擬內(nèi)存空間節(jié)點(diǎn)列表?VirtualSpaceList

    22. 虛擬內(nèi)存空間節(jié)點(diǎn)?VirtualSpaceNode?與?CompressedClassSpaceSize

    23. MetaChunk

    24. 類加載的入口?SystemDictionary?與保留所有?ClassLoaderData?的?ClassLoaderDataGraph

    25. 每個(gè)類加載器私有的?ClassLoaderData?以及?ClassLoaderMetaspace

    26. 管理正在使用的?MetaChunk?的?MetaspaceArena

    27. 元空間內(nèi)存分配流程(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 9.元空間內(nèi)存分配流程開始)

    28. ClassLoaderData?回收

    29. ChunkHeaderPool?池化?MetaChunk?對象

    30. ChunkManager?管理空閑的?MetaChunk

    31. 類加載器到?MetaSpaceArena?的流程

    32. 從?MetaChunkArena?普通分配 - 整體流程

    33. 從?MetaChunkArena?普通分配 -?FreeBlocks?回收老的?current chunk?與用于后續(xù)分配的流程

    34. 從?MetaChunkArena?普通分配 - 嘗試從?FreeBlocks?分配

    35. 從?MetaChunkArena?普通分配 - 嘗試擴(kuò)容?current chunk

    36. 從?MetaChunkArena?普通分配 - 從?ChunkManager?分配新的?MetaChunk

    37. 從?MetaChunkArena?普通分配 - 從?ChunkManager?分配新的?MetaChunk?- 從?VirtualSpaceList?申請新的?RootMetaChunk

    38. 從?MetaChunkArena?普通分配 - 從?ChunkManager?分配新的?MetaChunk?- 將?RootMetaChunk?切割成為需要的?MetaChunk

    39. MetaChunk?回收 - 不同情況下,?MetaChunk?如何放入?FreeChunkListVector

    40. 什么時(shí)候用到元空間,以及釋放時(shí)機(jī)

    41. 元空間保存什么

    42. 什么是元數(shù)據(jù),為什么需要元數(shù)據(jù)

    43. 什么時(shí)候用到元空間,元空間保存什么

    44. 元空間的核心概念與設(shè)計(jì)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 8.元空間的核心概念與設(shè)計(jì)開始)

    45. 元空間分配與回收流程舉例(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 10.元空間分配與回收流程舉例開始)

    46. 元空間大小限制與動(dòng)態(tài)伸縮(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 11.元空間分配與回收流程舉例開始)

    47. jcmd VM.metaspace?元空間說明、元空間相關(guān) JVM 日志以及元空間 JFR 事件詳解(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 12.元空間各種監(jiān)控手段開始)

  5. JVM 線程內(nèi)存設(shè)計(jì)(重點(diǎn)研究 Java 線程)(全網(wǎng)最硬核 JVM 內(nèi)存解析 - 13.JVM 線程內(nèi)存設(shè)計(jì)開始)

    1. 解釋執(zhí)行與編譯執(zhí)行時(shí)候的判斷(x86為例)

    2. 一個(gè) Java 線程 Xss 最小能指定多大

    3. JVM 中有哪幾種線程,對應(yīng)線程棧相關(guān)的參數(shù)是什么(涉及 JVM 參數(shù):ThreadStackSize,VMThreadStackSize,CompilerThreadStackSize,StackYellowPages,StackRedPages,StackShadowPages,StackReservedPages,RestrictReservedStack

    4. Java 線程棧內(nèi)存的結(jié)構(gòu)

    5. Java 線程如何拋出的 StackOverflowError

3. Java 堆內(nèi)存相關(guān)設(shè)計(jì)

3.4. 壓縮對象指針相關(guān)機(jī)制 - UseCompressedOops

3.4.1. 壓縮對象指針存在的意義

現(xiàn)代機(jī)器大部分是 64 位的,JVM 也從 9 開始僅提供 64 位的虛擬機(jī)。在 JVM 中,一個(gè)對象指針,對應(yīng)進(jìn)程存儲這個(gè)對象的虛擬內(nèi)存的起始位置,也是 64 位大小:

我們知道,對于 32 位尋址,最大僅支持 4GB 內(nèi)存的尋址,這在現(xiàn)在的 JVM 很可能不夠用,可能僅僅堆大小就超過 4GB。所以目前對象指針一般是 64 位大小來支持大內(nèi)存。但是,這相對 32 位指針尋址來說,性能上卻有衰減。我們知道,CPU 僅能處理寄存器里面的數(shù)據(jù),寄存器與內(nèi)存之間,有很多層 CPU 緩存,雖然內(nèi)存越來越便宜也越來越大,但是 CPU 緩存并沒有變大,這就導(dǎo)致如果使用 64 位的指針尋址,相對于之前 32 位的,CPU 緩存能容納的指針個(gè)數(shù)小了一倍。

Java 是面向?qū)ο蟮恼Z言,JVM 中最多的操作,就是對對象的操作,比如 load 一個(gè)對象的字段,store 一個(gè)對象的字段,這些都離不開訪問對象指針。所以 JVM 想盡可能的優(yōu)化對象指針,這就引入了壓縮對象指針,讓對象指針在條件滿足的情況下保持原來的 32 位。

對于 32 位的指針,假設(shè)每一個(gè) 1 代表 1 字節(jié),那么可以描述 0~2^32-1 這 2^32 字節(jié)也就是 4 GB 的虛擬內(nèi)存。

image

如果我讓每一個(gè) 1 代表 8 字節(jié)呢?也就是讓這塊虛擬內(nèi)存是 8 字節(jié)對齊,也就是我在使用這塊內(nèi)存時(shí)候,最小的分配單元就是 8 字節(jié)。對于 Java 堆內(nèi)存,也就是一個(gè)對象占用的空間,必須是 8 字節(jié)的整數(shù)倍,不足的話會填充到 8 字節(jié)的整數(shù)倍用于保證對齊。這樣最多可以描述 2^32 * 8 字節(jié)也就是 32 GB 的虛擬內(nèi)存。

image

這就是壓縮指針的原理,上面提到的相關(guān) JVM 參數(shù)是:ObjectAlignmentInBytes,這個(gè) JVM 參數(shù)表示 Java 堆中的每個(gè)對象,需要按照幾字節(jié)對齊,也就是堆按照幾字節(jié)對齊,值范圍是 8 ~ 256,必須是 2 的 n 次方,因?yàn)?2 的 n 次方能簡化很多運(yùn)算,例如對于 2 的 n 次方取余數(shù)就可以簡化成對于 2 的 n 次方減一取與運(yùn)算,乘法和除法可以簡化移位。

如果配置最大堆內(nèi)存超過 32 GB(當(dāng) JVM 是 8 字節(jié)對齊),那么壓縮指針會失效其實(shí)不是超過 32GB,會略小于 32GB 的時(shí)候就會失效,還有其他的因素影響,下一節(jié)會講到)。 但是,這個(gè) 32 GB 是和字節(jié)對齊大小相關(guān)的,也就是?-XX:ObjectAlignmentInBytes=8?配置的大小(默認(rèn)為8字節(jié),也就是 Java 默認(rèn)是 8 字節(jié)對齊)。如果你配置?-XX:ObjectAlignmentInBytes=16,那么最大堆內(nèi)存超過 64 GB 壓縮指針才會失效,如果你配置?-XX:ObjectAlignmentInBytes=32,那么最大堆內(nèi)存超過 128 GB 壓縮指針才會失效.

3.4.2. 壓縮對象指針與壓縮類指針的關(guān)系演進(jìn)

老版本中,?UseCompressedClassPointers?取決于?UseCompressedOops,即壓縮對象指針如果沒開啟,那么壓縮類指針也無法開啟。但是從 Java 15 Build 23 開始,?UseCompressedClassPointers?已經(jīng)不再依賴?UseCompressedOops?了,兩者在大部分情況下已經(jīng)獨(dú)立開來。除非在 x86 的 CPU 上面啟用 JVM Compiler Interface(例如使用 GraalVM)。參考 JDK ISSUE:https://bugs.openjdk.java.net/browse/JDK-8241825 - Make compressed oops and compressed class pointers independent (x86_64, PPC, S390)?以及源碼:

  • https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/x86/globalDefinitions_x86.hpp#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS EnableJVMCI?在 x86 CPU 上,UseCompressedClassPointers?是否依賴?UseCompressedOops?取決于是否啟用了 JVMCI,默認(rèn)使用的 JVM 發(fā)布版,EnableJVMCI 都是 false

  • https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/arm/globalDefinitions_arm.hpp#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS false?在 ARM CPU 上,UseCompressedClassPointers?不依賴?UseCompressedOops

  • https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/ppc/globalDefinitions_ppc.hpp#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS false?在 PPC CPU 上,UseCompressedClassPointers?不依賴?UseCompressedOops

  • https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/cpu/s390/globalDefinitions_s390.hpp#define COMPRESSED_CLASS_POINTERS_DEPENDS_ON_COMPRESSED_OOPS false?在 S390 CPU 上,UseCompressedClassPointers?不依賴?UseCompressedOops

3.4.3. 壓縮對象指針的不同模式與尋址優(yōu)化機(jī)制

對象指針與壓縮對象指針如何轉(zhuǎn)化,我們先來思考一些問題。通過第二章的分析我們知道,每個(gè)進(jìn)程有自己的虛擬地址空間,并且從 0 開始的一些低位空間,是給進(jìn)程的一些系統(tǒng)調(diào)用保留的空間,例如?0x0000 0000 0000 0000?~?0x0000 0000 0040 0000?是保留區(qū)不可使用,如下圖所示(本圖來自于bin 神(公眾號:bin 的技術(shù)小屋)的系列文章:一步一圖帶你深入理解 Linux 虛擬內(nèi)存管理)

image

進(jìn)程可以申請的空間,是上圖所示的原生堆空間。所以,JVM 進(jìn)程的虛擬內(nèi)存空間,肯定不會從?0x0000 0000 0000 0000?開始。不同的操作系統(tǒng),這個(gè)原生堆空間的起始不太一樣,這里我們不關(guān)心具體的位置,我們僅知道一點(diǎn):JVM 需要從虛擬內(nèi)存的某一點(diǎn)開始申請內(nèi)存,并且,需要預(yù)留出足夠多的空間,給可能的一些系統(tǒng)調(diào)用機(jī)制使用,比如前面我們 native memory tracking 中看到的一些 malloc 內(nèi)存,其實(shí)某些就在這個(gè)預(yù)留空間中分配的。一般的,JVM 會優(yōu)先考慮 Java 堆的內(nèi)存在原生堆分配,之后再在原生堆分配其他的,例如元空間,代碼緩存空間等等。

JVM 在 Reserve 分配 Java 堆空間的時(shí)候,會一下子 Reserve 最大 Java 堆空間的大小,然后在此基礎(chǔ)上 Reserve 分配其他的存儲空間。之后分配 Java 對象,在 Reserve 的 Java 堆內(nèi)存空間內(nèi) Commit 然后寫入數(shù)據(jù)映射物理內(nèi)存分配 Java 對象。根據(jù)前面說的 Java 堆大小的伸縮策略,決定繼續(xù) Commit 占用更多物理內(nèi)存還是 UnCommit 釋放物理內(nèi)存:

image

Java 是一個(gè)面向?qū)ο蟮恼Z言,JVM 中執(zhí)行最多的就是訪問這些對象,在 JVM 的各種機(jī)制中,必須無時(shí)無刻考慮怎么優(yōu)化訪問這些對象的速度,對于壓縮對象指針,JVM 就考慮了很多優(yōu)化。如果我們要使用壓縮對象指針,那么需要將這個(gè) 64 位的地址,轉(zhuǎn)換為 32 位的地址。然后在讀取壓縮對象指針?biāo)赶虻膶ο笮畔⒌臅r(shí)候,需要將這個(gè) 32 位的地址,解析為 64 位的地址之后尋址讀取。這個(gè)轉(zhuǎn)換公式,如下所示:

  1. 64 位地址 = 基址 + (壓縮對象指針 << 對象對齊偏移)

  2. 壓縮對象指針 = (64 位地址 - 基址) >> 對象對齊偏移

基址其實(shí)就是對象地址的開始,注意,這個(gè)基址不一定是 Java 堆的開始地址,我們后面就會看到。對象對齊偏移與前面提到的?ObjectAlignmentInBytes?相關(guān),例如?ObjectAlignmentInBytes=8?的情況下,對象對齊偏移就是 3 (因?yàn)?8 是 2 的 3 次方)。我們針對這個(gè)公式進(jìn)行優(yōu)化

首先,我們考慮把基址和對象對齊偏移去掉,那么壓縮對象指針可以直接作為對象地址使用。什么情況下可以這樣呢?那么就是對象地址從 0 開始算,并且最大堆內(nèi)存 + Java 堆起始位置不大于 4GB。因?yàn)檫@種情況下,Java 堆中對象的最大地址不會超過 4GB,那么壓縮對象指針的范圍可以直接表示所有 Java 堆中的對象。可以直接使用壓縮對象指針作為對象實(shí)際內(nèi)存地址使用。這里為啥是最大堆內(nèi)存 + Java 堆起始位置不大于 4GB?因?yàn)榍懊娴姆治?,我們知道進(jìn)程可以申請的空間,是原生堆空間。所以,Java 堆起始位置,肯定不會從?0x0000 0000 0000 0000?開始。

image

如果最大堆內(nèi)存 + Java 堆起始位置大于 4GB,第一種優(yōu)化就不能用了,對象地址偏移就無法避免了。但是如果可以保證最大堆內(nèi)存 + Java 堆起始位置小于 32位 *?ObjectAlignmentInBytes,默認(rèn)?ObjectAlignmentInBytes=8?的情況即 32GB,我們還是可以讓基址等于 0,這樣?64 位地址 = (壓縮對象指針 << 對象對齊偏移)

image

但是,在ObjectAlignmentInBytes=8?的情況,如果最大堆內(nèi)存太大,接近 32GB,想要保證最大堆內(nèi)存 + Java 堆起始位置小于 32GB,那么 Java 堆起始位置其實(shí)就快接近 0 了,這顯然不行。所以在最大堆內(nèi)存接近 32GB 的時(shí)候,上面第二種優(yōu)化也就失效了。但是我們可以讓 Java 堆從一個(gè)與 32GB 地址完全不相交的地址開始,這樣加法就可以優(yōu)化為取或運(yùn)算,即64 位地址 = 基址 |(壓縮對象指針 << 對象對齊偏移)

image

最后,在ObjectAlignmentInBytes=8?的情況,如果用戶通過?HeapBaseMinAddress?自己指定了 Java 堆開始的地址,并且與 32GB 地址相交,并最大堆內(nèi)存 + Java 堆起始位置大于 32GB,但是最大堆內(nèi)存沒有超過 32GB,那么就無法優(yōu)化了,只能?64 位地址 = 基址 + (壓縮對象指針 << 對象對齊偏移)

image

總結(jié)下,上面我們說的那四種模式,對應(yīng) JVM 中的壓縮對象指針的四種模式(以下敘述基于?ObjectAlignmentInBytes=8?的情況,即默認(rèn)情況):

  1. 32-bit?壓縮指針模式:最大堆內(nèi)存 + Java 堆起始位置不大于 4GB(并且 Java 堆起始位置不能太?。?,64 位地址 = 壓縮對象指針

  2. Zero based?壓縮指針模式:最大堆內(nèi)存 + Java 堆起始位置不大于 32GB(并且 Java 堆起始位置不能太?。?,64 位地址 = (壓縮對象指針 << 對象對齊偏移)

  3. Non-zero disjoint?壓縮指針模式:最大堆內(nèi)存不大于 32GB,由于要保證 Java 堆起始位置不能太小,最大堆內(nèi)存 + Java 堆起始位置大于 32GB,64 位地址 = 基址 |(壓縮對象指針 << 對象對齊偏移)

  4. Non-zero based?壓縮指針模式:用戶通過?HeapBaseMinAddress?自己指定了 Java 堆開始的地址,并且與 32GB 地址相交,并最大堆內(nèi)存 + Java 堆起始位置大于 32GB,但是最大堆內(nèi)存沒有超過 32GB,64 位地址 = 基址 + (壓縮對象指針 << 對象對齊偏移)

3.5. 為何預(yù)留第 0 頁,壓縮對象指針 null 判斷擦除的實(shí)現(xiàn)

前面我們知道,JVM 中的壓縮對象指針有四種模式。對于地址非從 0 開始的那兩種,即?Non-zero disjoint?和?Non-zero based?這兩種,堆的實(shí)際地址并不是從?HeapBaseMinAddress?開始,而是有一頁預(yù)留下來,被稱為第 0 頁,這一頁不映射實(shí)際內(nèi)存,如果訪問這一頁內(nèi)部的地址,會有 Segment Fault 異常。那么為什么要預(yù)留這一頁呢?主要是為了 null 判斷優(yōu)化,實(shí)現(xiàn) null 判斷擦除。

我們都知道,Java 中如果對于一個(gè) null 的引用變量進(jìn)行成員字段或者方法的訪問,會拋出?NullPointerException。但是,這個(gè)是如何實(shí)現(xiàn)的呢?我們的代碼中沒有明確的 null 判斷,如果是 null 就拋出?NullPointerException,但是 JVM 還是針對 null 可以拋出?NullPointerException?這個(gè) Java 異常??梢圆聹y出,JVM 可能在訪問每個(gè)引用變量進(jìn)行成員字段或者方法的時(shí)候,都會做這樣一個(gè)判斷:

if (o == null) { ? ? throw new NullPoniterException(); }

但是,如果每次訪問每個(gè)引用變量進(jìn)行成員字段或者方法的時(shí)候都做這樣一個(gè)判斷,是很低效率的行為。所以,在解釋執(zhí)行的時(shí)候,可能每次訪問每個(gè)引用變量進(jìn)行成員字段或者方法的時(shí)候都做這樣一個(gè)判斷。在代碼運(yùn)行一定次數(shù),進(jìn)入 C1,C2 的編譯優(yōu)化之后,這些 null 判斷可能會被擦除??赡懿脸陌ǎ?/p>

  1. 成員方法對于 this 的訪問,可以將 this 的 null 判斷擦除。

  2. 代碼中明確判斷了某個(gè)變量是否為 null,并且這個(gè)變量不是 volatile 的

  3. 前面已經(jīng)有了?a.something()?類似的訪問,并且?a?不是 volatile 的,后面?a.somethingElse()?就不用再做 null 檢查了

  4. 等等等等...

對于無法擦除的,JVM 傾向于做出一個(gè)假設(shè),即這個(gè)變量大概率不會為 null,JIT 優(yōu)化先直接將 null 判斷擦除。Java 中的 null,對應(yīng)壓縮對象指針的值為 0:

enum class narrowOop : uint32_t { null = 0 };

對于壓縮對象指針地址為 0 的地方進(jìn)行訪問,實(shí)際上就是針對前面我們討論的壓縮對象指針基址進(jìn)行訪問,在四種模式下:

  1. 32-bit?壓縮指針模式:就是對于?0x0000 0000 0000 0000?進(jìn)行訪問,但是前面我們知道,0x0000 0000 0000 0000?是保留區(qū)域,無法訪問,會有?Segment Fault?錯(cuò)誤,發(fā)出?SIGSEGV?信號

  2. Zero based?壓縮指針模式:就是對于?0x0000 0000 0000 0000?進(jìn)行訪問,但是前面我們知道,0x0000 0000 0000 0000?是保留區(qū)域,無法訪問,會有?Segment Fault?錯(cuò)誤,發(fā)出?SIGSEGV?信號

  3. Non-zero disjoint?壓縮指針模式:就是對于基址進(jìn)行訪問,但是前面我們知道,基址 + JVM 系統(tǒng)頁大小為僅 Reserve 但是不會 commit 的預(yù)留區(qū)域,無法訪問,會有?Segment Fault?錯(cuò)誤,發(fā)出?SIGSEGV?信號

  4. Non-zero based?壓縮指針模式:就是對于基址進(jìn)行訪問,但是前面我們知道,基址 + JVM 系統(tǒng)頁大小為僅 Reserve 但是不會 commit 的預(yù)留區(qū)域,無法訪問,會有?Segment Fault?錯(cuò)誤,發(fā)出?SIGSEGV?信號

對于非壓縮對象指針的情況,更簡單,非壓縮對象指針 null 就是?0x0000 0000 0000 0000,就是對于?0x0000 0000 0000 0000?進(jìn)行訪問,但是前面我們知道,0x0000 0000 0000 0000?是保留區(qū)域,無法訪問,會有?Segment Fault?錯(cuò)誤,發(fā)出?SIGSEGV?信號

可以看出,如果 JIT 優(yōu)化將 null 判斷擦除,那么在真的遇到 null 的時(shí)候,會有?Segment Fault?錯(cuò)誤,發(fā)出?SIGSEGV?信號。JVM 有對于?SIGSEGV?信號的處理:

//這是在 AMD64 CPU 下的代碼 } else if ( ? ? ? ? //如果信號是 SIGSEGV ? ? ? ? sig == SIGSEGV && ? ? ? ? //并且是由于遇到擦除 null 判斷的地方遇到 null 導(dǎo)致的 SIGSEGV(后面我們看到很多其他地方用到了 SIGSEGV) ? ? ? ? MacroAssembler::uses_implicit_null_check(info->si_addr) ? ? ? ? ) { ? // 如果是由于遇到 null 導(dǎo)致的 SIGSEGV,那么就需要評估下,是否要繼續(xù)擦除這里的 null 判斷了 ? stub = SharedRuntime::continuation_for_implicit_exception(thread, pc, SharedRuntime::IMPLICIT_NULL); }

JVM 不僅 null 檢查擦除使用了?SIGSEGV?信號,還有其他地方也用到了(例如后面我們會詳細(xì)分析的?StackOverflowError?的實(shí)現(xiàn))。所以,我們需要通過判斷下發(fā)生?SIGSEGV?信號的地址,如果地址是我們上面列出的范圍,則是擦除 null 判斷的地方遇到 null 導(dǎo)致的?SIGSEGV

bool MacroAssembler::uses_implicit_null_check(void* address) { ? uintptr_t addr = reinterpret_cast<uintptr_t>(address); ? uintptr_t page_size = (uintptr_t)os::vm_page_size(); #ifdef _LP64 ? //如果壓縮對象指針開啟 ? if (UseCompressedOops && CompressedOops::base() != NULL) { ? ? //如果存在預(yù)留頁(第 0 頁),起點(diǎn)是基址 ? ? uintptr_t start = (uintptr_t)CompressedOops::base(); ? ? //如果存在預(yù)留頁(第 0 頁),終點(diǎn)是基址 + 頁大小 ? ? uintptr_t end = start + page_size; ? ? //如果地址范圍在第 0 頁,則是擦除 null 判斷的地方遇到 null 導(dǎo)致的 `SIGSEGV` ? ? if (addr >= start && addr < end) { ? ? ? return true; ? ? } ? } #endif ? //如果在整個(gè)虛擬空間的第 0 頁,則是擦除 null 判斷的地方遇到 null 導(dǎo)致的 `SIGSEGV` ? return addr < page_size; }

我們分別代入壓縮對象指針的 4 種情況:

  1. 32-bit?壓縮指針模式:就是對于?0x0000 0000 0000 0000?進(jìn)行訪問,地址位于第 0 頁,uses_implicit_null_check?返回 true

  2. Zero based?壓縮指針模式:就是對于?0x0000 0000 0000 0000?進(jìn)行訪問,地址位于第 0 頁,uses_implicit_null_check?返回 true

  3. Non-zero disjoint?壓縮指針模式:就是對于基址進(jìn)行訪問,地址位于第 0 頁,uses_implicit_null_check?返回 true

  4. Non-zero based?壓縮指針模式:就是對于基址進(jìn)行訪問,地址位于第 0 頁,uses_implicit_null_check?返回 true

對于非壓縮對象指針的情況,更簡單,非壓縮對象指針 null 就是?0x0000 0000 0000 0000,就是對于基址進(jìn)行訪問,地址位于第 0 頁,uses_implicit_null_check?返回 true

這樣,我們知道,JIT 可能會將 null 檢查擦除,通過?SIGSEGV?信號拋出?NullPointerException。但是,通過?SIGSEGV?信號要經(jīng)過系統(tǒng)調(diào)用,系統(tǒng)調(diào)用是一個(gè)很低效的行為,我們需要盡量避免(對于抄襲狗就不不必了)。但是這里的假設(shè)就是大概率不為 null,所以使用系統(tǒng)調(diào)用也無所謂。但是如果一個(gè)地方經(jīng)常出現(xiàn) null,JIT 就會考慮不這么優(yōu)化了,將代碼去優(yōu)化并重新編譯,不再擦除 null 檢查而是使用顯式 null 檢查拋出。

最后,我們知道了,要預(yù)留第 0 頁,不映射內(nèi)存,實(shí)際就是為了讓對于基址進(jìn)行訪問可以觸發(fā) Segment Fault,JVM 會捕捉這個(gè)信號,查看觸發(fā)這個(gè)信號的內(nèi)存地址是否屬于第一頁,如果屬于那么 JVM 就知道了這個(gè)是對象為 null 導(dǎo)致的。不過從前面看,我們其實(shí)只是為了不映射基址對應(yīng)的地址,那為啥要保留一整頁呢?這個(gè)是處于內(nèi)存對齊與尋址訪問速度的考量,里面映射物理內(nèi)存都是以頁為單位操作的,所以內(nèi)存需要按頁對齊。

3.6. 結(jié)合壓縮對象指針與前面提到的堆內(nèi)存限制的初始化的關(guān)系

前面我們說明了不手動(dòng)指定三個(gè)指標(biāo)的情況下,這三個(gè)指標(biāo) (MinHeapSize,MaxHeapSize,InitialHeapSize) 是如何計(jì)算的,但是沒有涉及壓縮對象指針。如果壓縮對象指針開啟,那么堆內(nèi)存限制的初始化之后,會根據(jù)參數(shù)確定壓縮對象指針是否開啟:

  1. 首先,確定 Java 堆的起始位置:

  2. 第一步,在不同操作系統(tǒng)不同 CPU 環(huán)境下,HeapBaseMinAddress?的默認(rèn)值不同,大部分環(huán)境下是?2GB,例如對于 Linux x86 環(huán)境,查看源碼:https://github.com/openjdk/jdk/blob/jdk-21%2B3/src/hotspot/os_cpu/linux_x86/globals_linux_x86.hpp:define_pd_global(size_t, HeapBaseMinAddress, 2*G);

  3. 將?DefaultHeapBaseMinAddress?設(shè)置為?HeapBaseMinAddress?的默認(rèn)值,即?2GB

  4. 如果用戶在啟動(dòng)參數(shù)中指定了?HeapBaseMinAddress,如果?HeapBaseMinAddress?小于?DefaultHeapBaseMinAddress,將?HeapBaseMinAddress?設(shè)置為?DefaultHeapBaseMinAddress

  5. 計(jì)算壓縮對象指針堆的最大堆大小:

  6. 讀取對象對齊大小?ObjectAlignmentInBytes?參數(shù)的值,默認(rèn)為 8

  7. 對?ObjectAlignmentInBytes?取 2 的對數(shù),記為?LogMinObjAlignmentInBytes

  8. 將 32 位左移?LogMinObjAlignmentInBytes?得到?OopEncodingHeapMax?即不考慮預(yù)留區(qū)的最大堆大小

  9. 如果需要預(yù)留區(qū),即?Non-Zero Based Disjoint?以及?Non-Zero Based?這兩種模式下,需要刨除掉預(yù)留區(qū)即第 0 頁的大小,即?OopEncodingHeapMax?- 第 0 頁的大小

  10. 讀取當(dāng)前 JVM 配置的最大堆大小(前面我們分析了最大堆大小如何計(jì)算出來的)

  11. 如果 JVM 配置的最大堆小于壓縮對象指針堆的最大堆大小,并且沒有通過 JVM 啟動(dòng)參數(shù)明確關(guān)閉壓縮對象指針,則開啟壓縮對象指針。否則,關(guān)閉壓縮對象指針。你洗稿的樣子真丑。

  12. 如果壓縮對象指針關(guān)閉,根據(jù)前面分析過的是否壓縮類指針強(qiáng)依賴壓縮對象指針,如果是,關(guān)閉壓縮類指針

3.7. 使用 jol + jhsdb + JVM 日志查看壓縮對象指針與 Java 堆驗(yàn)證我們前面的結(jié)論

引入 jol 依賴:

<dependency> ? ? <groupId>org.openjdk.jol</groupId> ? ? <artifactId>jol-core</artifactId> ? ? <version>0.16</version> </dependency>

編寫代碼:

package test; import org.openjdk.jol.info.ClassLayout; public class TestClass { ? ? //TestClass 對象僅僅包含一個(gè)字段 next(洗稿狗滾) ? ? private String next = new String(); ? ? public static void main(String[] args) throws InterruptedException { ? ? ? ? //在棧上新建一個(gè) tt 本地變量,指向一個(gè)在堆上新建的 TestClass 對象 ? ? ? ? final TestClass tt = new TestClass(); ? ? ? ? //使用 jol 輸出 tt 指向的對象的結(jié)構(gòu)(抄襲不得好死) ? ? ? ? System.out.println(ClassLayout.parseInstance(tt).toPrintable()); ? ? ? ? //無限等待,防止程序退出 ? ? ? ? Thread.currentThread().join(); ? ? } }

3.7.1. 驗(yàn)證?32-bit?壓縮指針模式

接下來我們先測試第一種壓縮對象指針模式(32-bit)的情況,即 Java 堆位于?0x0000 0000 0000 0000 ~ 0x 0000 0001 0000 0000(0~4GB) 之間的情況,使用下面的啟動(dòng)參數(shù)啟動(dòng)這個(gè)程序:

-Xmx32M -Xlog:coops*=debug

其中?-Xlog:coops*=debug?代表查看 JVM 日志中帶 coops 標(biāo)簽的 debug 日志。這個(gè)日志會告訴你堆的起始虛擬內(nèi)存位置,以及堆 reserved 的空間大小,以及 壓縮對象指針的模式。

啟動(dòng)后,查看日志輸出:

[0.006s][debug][gc,heap,coops] Heap address: 0x00000000fe000000, size: 32 MB, Compressed Oops mode: 32-bit test.TestClass object internals:個(gè)人愛好鉆研技術(shù)分享,請抄襲狗滾開。 OFF ?SZ ? ? ? ? ? ? ? TYPE DESCRIPTION ? ? ? ? ? ? ? VALUE ? 0 ? 8 ? ? ? ? ? ? ? ? ? ?(object header: mark) ? ? 0x0000000000000001 (non-biasable; age: 0) ? 8 ? 4 ? ? ? ? ? ? ? ? ? ?(object header: class) ? ?0x00c01000 ?12 ? 4 ? java.lang.String TestClass.next ? ? ? ? ? ?(object) Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

第一行日志告訴我們,堆的起始位置是?0x0000 0000 fe00 0000,大小是 32 MB,壓縮對象指針模式是?32-bit。其中?0x0000 0000 fe00 0000?加上 32 MB,結(jié)果就是 4GB?0x0000 0001 0000 0000??梢钥闯鲋罢f的 Java 堆會從界限減去最大堆大小的位置開始 reserve 的結(jié)論是對的。在這種情況下,0x0000 0000 0000 0000 ~ 0x0000 0000 fdff ffff?的內(nèi)存就給之前所說的進(jìn)程系統(tǒng)調(diào)用以及原生內(nèi)存分配使用。

后面的日志是關(guān)于 jol 輸出對象結(jié)構(gòu)的,可以看出目前這個(gè)對象包含一個(gè) markword (0x0000000000000001),一個(gè)壓縮類指針(0x00c01000),以及字段?next。我們使用 jhsdb 看一下進(jìn)程的具體虛擬內(nèi)存的內(nèi)容驗(yàn)證下

首先打開 jhsdb gui 模式:jhsdb hsdb

image

之后 "File" -> "Attach to Hotspot Process",輸入你的 JVM 進(jìn)程號:

image

成功 Attach 之后,可以看到面板上有你的 JVM 進(jìn)程的所有線程,目前我們就看 main 線程即可,點(diǎn)擊 main 線程,之后點(diǎn)擊下圖紅框的按鈕(查看線程棧內(nèi)存):

image

之后我們在 main 線程棧內(nèi)存中可以找到代碼中的本地變量 tt:

image

這里我們可以看到變量 tt 存儲的值,其實(shí)就是對象的地址,我們打開 "Tools" -> "Memory Viewer",這個(gè)是進(jìn)程虛擬內(nèi)存查看器,可以查看內(nèi)存地址的實(shí)際值。還有 "Tools" -> "Inspector",將地址轉(zhuǎn)換為 JVM 的 C++ 對應(yīng)對象。在這兩個(gè)窗口都輸入上面在 main 線程棧內(nèi)存看到的本地變量 tt 的值:

image

從上圖我們可以看到,tt 保存的對象,對象位置,也就是對象起始地址是?0x00000000ffec7450,對象頭是?0x0000 0000 ffec 7450 ~ 0x0000 0000 ffec 7457,保存的值是?0x0000 0000 0000 0001,這個(gè)和上面 jol 輸出的一模一樣。壓縮類指針是?0x0000 0000 ffec 7458 ~ 0x0000 0000 ffec 745b,保存的值是?0x00c0 1000,這個(gè)和上面 jol 輸出的壓縮類指針地址一模一樣。之后是 next 字段值,范圍是?0x0000 0000 ffec 745c ~ 0x0000 0000 ffec 745f,保存的值是?0xffec 7460,對應(yīng)的字符串對象實(shí)際地址也是?0x0000 0000 ffec 7460。可以看出,和我們之前說的?32-bit?模式的壓縮類指針的特點(diǎn)一模一樣。

3.7.2. 驗(yàn)證?Zero based?壓縮指針模式

下一個(gè)我們嘗試?Zero based?模式,使用參數(shù)?-Xmx2050M -Xlog:coops*=debug?啟動(dòng)程序(和你的平臺相關(guān),建議你查看下在你的平臺?HeapBaseMinAddress?默認(rèn)的大小,一般對于 x86 都是 2G,所以指定一個(gè)大于?4G - 2G = 2G?的最大堆內(nèi)存大小的值即可),日志輸出是:

[0.006s][debug][gc,heap,coops] Heap address: 0x000000077fe00000, size: 2050 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 ? ? ? ? ? ? ? ? ? ? ?洗稿的狗也遇到不少 test.TestClass object internals: OFF ?SZ ? ? ? ? ? ? ? TYPE DESCRIPTION ? ? ? ? ? ? ? VALUE ? 0 ? 8 ? ? ? ? ? ? ? ? ? ?(object header: mark) ? ? 0x0000000000000009 (non-biasable; age: 1) ? 8 ? 4 ? ? ? ? ? ? ? ? ? ?(object header: class) ? ?0x00c01000 ?12 ? 4 ? java.lang.String TestClass.next ? ? ? ? ? ?(object) Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

這次我們發(fā)現(xiàn),Java 堆從?0x0000 0007 7fe0 0000?開始了,如果你用?0x0000 0007 7fe0 0000?加上 2050 MB 就會發(fā)現(xiàn)正好等于 32GB,可以看出之前說的 Java 堆會從界限減去最大堆大小的位置開始 reserve 的結(jié)論是對的。

后面的日志是關(guān)于 jol 輸出對象結(jié)構(gòu)的,可以看出目前這個(gè)對象包含一個(gè) markword(0x0000000000000009,由于我的程序啟動(dòng)后輸出 jol 日志之前經(jīng)過了一次 GC,所以當(dāng)前值與前面一個(gè)例子的不一樣),一個(gè)壓縮類指針(0x00c01000),以及字段?next

我們使用 jhsdb 看一下進(jìn)程的具體虛擬內(nèi)存的內(nèi)容驗(yàn)證下目前的壓縮對象指針的內(nèi)容,前面的步驟與上一個(gè)例子一樣,我們直接看最后的:

image

如上圖所示,tt 保存的對象,從?0x0000 0007 9df7 2640?開始,我們找到 next 字段,它保存的值是?0xf3be ed80,將其左移三位,就是?0x0000 0007 9df7 6c00(inspector 中顯示的是幫你解壓縮之后的對象地址,Memory Viewer 中是虛擬內(nèi)存實(shí)際保存的值)

接下來我們試一下通過?HeapBaseMinAddress?讓第一個(gè)例子也變成?Zero based?模式。使用下面的啟動(dòng)參數(shù)?-Xmx32M -Xlog:coops*=debug -XX:HeapBaseMinAddress=4064M,其中?4064MB + 32MB = 4GB,啟動(dòng)后可以通過日志發(fā)現(xiàn)模式還是?32-bit[0.005s][debug][gc,heap,coops] Heap address: 0x00000000fe000000, size: 32 MB, Compressed Oops mode: 32-bit。其中?0x00000000fe000000?就是?4064MB,與啟動(dòng)參數(shù)配置的一致。使用下面的啟動(dòng)參數(shù)?-Xmx32M -Xlog:coops*=debug -XX:HeapBaseMinAddress=4065M,可以看到日志:

[0.005s][debug][gc,heap,coops] Heap address: 0x00000000fe200000, size: 32 MB, Compressed Oops mode: Zero based, Oop shift amount: 3 ?test.TestClass object internals: OFF ?SZ ? ? ? ? ? ? ? TYPE DESCRIPTION ? ? ? ? ? ? ? VALUE ? 0 ? 8 ? ? ? ? ? ? ? ? ? ?(object header: mark) ? ? 0x0000000000000001 (non-biasable; age: 0) ? 8 ? 4 ? ? ? ? ? ? ? ? ? ?(object header: class) ? ?0x00c01000 ?12 ? 4 ? java.lang.String TestClass.next ? ? ? ? ? ?(object) Instance size: 16 bytes chaoxi你妹啊,抄襲能給你賺幾個(gè)錢,別為了這點(diǎn)镚子敗人品了 Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可以看模式變?yōu)?Zero based,堆的起始點(diǎn)是?0x00000000fe200000?等于?4066MB,與我們的啟動(dòng)參數(shù)不符,是因?yàn)檫@個(gè)起始位置有對齊策略導(dǎo)致的,與使用的 GC 也是相關(guān)的,這個(gè)等我們以后分析 GC 的時(shí)候再關(guān)心。

3.7.3. 驗(yàn)證?Non-zero disjoint?壓縮指針模式

接下來我們來看下一個(gè)模式?Non-zero disjoint,使用以下參數(shù)?-Xmx31G -Xlog:coops*=debug?啟動(dòng)程序,日志輸出為:

[0.007s][debug][gc,heap,coops] Protected page at the reserved heap base: 0x0000001000000000 / 16777216 bytes [0.007s][debug][gc,heap,coops] Heap address: 0x0000001001000000, size: 31744 MB, Compressed Oops mode: Non-zero disjoint base: 0x0000001000000000, Oop shift amount: 3 ?test.TestClass object internals: OFF ?SZ ? ? ? ? ? ? ? TYPE DESCRIPTION ? ? ? ? ? ? ? VALUE ? ?0 ? 8 ? ? ? ? ? ? ? ? ? ?(object header: mark) ? ? 0x0000000000000001 (non-biasable; age: 0) ? 8 ? 4 ? ? ? ? ? ? ? ? ? ?(object header: class) ? ?0x00c01000 ?12 ? 4 ? java.lang.String TestClass.next ? ? ? ? ? ?(object) Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可以看到,保護(hù)頁大小為 16MB(16777216 bytes)chaoxi你妹啊,抄襲能給你賺幾個(gè)錢,別為了這點(diǎn)镚子敗人品了,實(shí)際 Java 堆開始的地址是?0x0000 0010 0100 0000。并且,基址也不再是 0(Non-zero disjoint base,而是與 32GB 完全不相交的地址?0x0000001000000000),可以將加法優(yōu)化為或運(yùn)算。后面 jol 輸出對象結(jié)構(gòu),可以看出目前這個(gè)對象包含一個(gè) markword(0x0000000000000001),一個(gè)壓縮類指針(0x00c01000),以及字段?next。

我們使用 jhsdb 看一下進(jìn)程的具體虛擬內(nèi)存的內(nèi)容驗(yàn)證下目前的壓縮對象指針的內(nèi)容,前面的步驟與上一個(gè)例子一樣,我們直接看最后的:

image

如上圖所示,tt 保存的對象,從?0x000000102045ab90?開始,我們找到 next 字段,它保存的值是?0x0408 b574,將其左移三位,就是?0x0000 0000 2045 aba0(inspector 中顯示的是幫你解壓縮之后的對象地址,Memory Viewer 中是虛擬內(nèi)存實(shí)際保存的值),然后對基址 ``0x0000 0010 0000 0000取或運(yùn)算,得到 next 指向的字符串對象的實(shí)際地址0x0000 0010 2045 aba0`,計(jì)算結(jié)果與 inspector 中顯示的 next 解析結(jié)果一致。

3.7.4. 驗(yàn)證?Non-zero based?壓縮指針模式

最后,我們來看最后一種模式,即?Non-zero based,使用以下參數(shù)?-Xmx31G -Xlog:coops*=debug -XX:HeapBaseMinAddress=2G?啟動(dòng)程序,日志輸出為:

[0.005s][debug][gc,heap,coops] Protected page at the reserved heap base: 0x0000000080000000 / 16777216 bytes [0.005s][debug][gc,heap,coops] Heap address: 0x0000000081000000, size: 31744 MB, Compressed Oops mode: Non-zero based: 0x0000000080000000, Oop shift amount: 3 test.TestClass object internals: OFF ?SZ ? ? ? ? ? ? ? TYPE DESCRIPTION ? ? ? ? ? ? ? VALUE ? 0 ? 8 ? ? ? ? ? ? ? ? ? ?(object header: mark) ? ? 0x0000000000000001 (non-biasable; age: 0) ? 8 ? 4 ? ? ? ? ? ? ? ? ? ?(object header: class) ? ?0x00c01000 ?12 ? 4 ? java.lang.String TestClass.next ? ? ? ? ? ?(object) Instance size: 16 bytes Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

可以看到,保護(hù)頁大小為 16MB(16777216 bytes),實(shí)際 Java 堆開始的地址是?0x0000 0000 8100 0000。并且,基址也不再是 0(Non-zero based:0x0000000080000000)。后面 jol 輸出對象結(jié)構(gòu),可以看出目前這個(gè)對象包含一個(gè) markword(0x0000000000000001),一個(gè)壓縮類指針(0x00c01000),以及字段?next

我們使用 jhsdb 看一下進(jìn)程的具體虛擬內(nèi)存的內(nèi)容驗(yàn)證下目前的壓縮對象指針的內(nèi)容,前面的步驟與上一個(gè)例子一樣,我們直接看最后的:

image

如上圖所示,tt 保存的對象,從?0x00000000a0431f10?開始,我們找到 next 字段,它保存的值是?0x0408 63e4,將其左移三位,就是?0x0000 0000 2043 1f20(inspector 中顯示的是幫你解壓縮之后的對象地址,Memory Viewer 中是虛擬內(nèi)存實(shí)際保存的值),然后加上基址 ``0x0000 0000 8000 0000(其實(shí)就是 2GB,是我們在-XX:HeapBaseMinAddress=2G指定的 ),得到 next 指向的字符串對象的實(shí)際地址0x0000 0000 a043 1f20`,計(jì)算結(jié)果與 inspector 中顯示的 next 解析結(jié)果一致。不要偷取他人的勞動(dòng)成果

微信搜索“干貨滿滿張哈希”關(guān)注公眾號,加作者微信,每日一刷,輕松提升技術(shù),斬獲各種offer
我會經(jīng)常發(fā)一些很好的各種框架的官方社區(qū)的新聞視頻資料并加上個(gè)人翻譯字幕到如下地址(也包括上面的公眾號),歡迎關(guān)注:


全網(wǎng)最硬核 JVM 內(nèi)存解析 - 5.壓縮對象指針相關(guān)機(jī)制的評論 (共 條)

分享到微博請遵守國家法律
新兴县| 德惠市| 沁阳市| 玉树县| 西和县| 津南区| 廊坊市| 孝义市| 通化市| 遂昌县| 巨野县| 保德县| 金沙县| 西华县| 津南区| 丹东市| 普兰店市| 布拖县| 庆云县| 车致| 合肥市| 全椒县| 盖州市| 桃园市| 神农架林区| 安岳县| 长岭县| 吉林省| 三门县| 鹿泉市| 呈贡县| 正定县| 香港| 高邑县| 瑞丽市| 卓资县| 师宗县| 斗六市| 巩义市| 青冈县| 外汇|