Gory details you sometimes wondered about, but then did not really wanted to know about
1. 導言
一個 Java 對象占用多少內(nèi)存黔漂,這是一個經(jīng)常被提及的問題。由于缺失 sizeof
操作[1]愈诚,人們只能去猜測淹辞,或者訴諸于傳聞。在這篇文章中竭翠,我們將會嘗試一探 Java 對象的究竟振坚。關(guān)于對象占用空間的許多技巧將會變得顯而易見,運行時的一些奇怪現(xiàn)象將會得到解釋斋扰,一些底層的性能行為將會更清晰渡八。
這篇文章有點兒長,所以你可能想分段閱讀传货。文中的每一章基本上是相互獨立的屎鳍,你可以隨時閱讀。與其他文章相比问裕,這篇文章沒有很仔細的評審逮壁,隨著大家閱讀文章指出問題,文章也會被更新修正粮宛。所以你需要自擔這些風險窥淆。
2. 更深入的設計和實踐問題 (DDIQ)
在一些章節(jié),你可能會看到關(guān)于設計實現(xiàn)問題討論的側(cè)邊條巍杈。這些并不能保證回答所有問題忧饭,但是嘗試回答最常見的問題。這些答案基于我自己的理解秉氧,所有可能會不準確不完整眷昆。如果你對本文有疑問,那么發(fā)郵件給我汁咏,這可能會產(chǎn)生另一個 DDIQ 側(cè)邊條亚斋。把這當做”聽眾問題“即可。
DDIQ: 我們確實需要閱讀這些側(cè)邊條么攘滩?
并不是帅刊。但是這些側(cè)邊條可能會讓你更好的理解其中的原因。第一次讀的時候可以忽略這些側(cè)邊條漂问。
3. 研究方法
本文基于 Hotspot JVM赖瞒,也就是 OpenJDK 及其衍生版中的默認 JVM女揭。如果你不清楚執(zhí)行的是哪個 JVM,那么很可能是 Hotspot栏饮。
3.1. 工具
首先需要選擇一個趁手的工具吧兔。理解工具能做什么和不能做什么是很重要的。
- 堆轉(zhuǎn)儲袍嬉。dump Java 堆境蔼,然后檢查,是一個不錯的辦法伺通。該方法取決于相信堆轉(zhuǎn)儲是運行時堆內(nèi)存的低級表示箍土。但是很不幸它不是:它是從真實的 Java 堆重建的幻影(通過 GC 自身)。如果你看一下HPROF 數(shù)據(jù)格式罐监,你將會明白這實際是很高級的:這不涉及字段偏移吴藻,也不涉及頭部信息,唯一的好處是帶有對象大小信息弓柱,然而這個信息也是有問題的沟堡。堆轉(zhuǎn)儲特別適用于查看整個對象圖,以及對象之間的關(guān)聯(lián)吆你,但是不適合查看對象本身弦叶。
- 通過 MXBeans 測量釋放和分配的內(nèi)存俊犯。當然我們可以分配很多對象妇多,然后看一下消耗了多少內(nèi)存裹驰。只要分配足夠多的對象腔召,我們就可以消除 TLAB 分配(和回收)弧关、后臺線程虛分配等引發(fā)的異常值谴咸。但是這不能使我們了解對象內(nèi)部:我們只能觀測對象的外觀大小收壕。這是一個做研究的好方法描馅,但是你需要正確地制定和測試假設竖伯,以得到一個可以解釋各種結(jié)果的可感知的對象模型蜡镶。
- 診斷 JVM 標志茫舶。但是等等械巡,因為 JVM 自身負責創(chuàng)建對象,那么它確切知道對象布局饶氏,我們”只“需要獲取到即可讥耗。
-XX:+PrintFieldLayout
是一個有用的參數(shù)。很不幸這個標志僅僅在 debug JVM 版本可用疹启。[2] - 戳入對象內(nèi)部的工具古程。很幸運通過
Class.getDeclaredFields
使用Unsafe.objectFieldOffset
可以獲取字段位置信息。這會遇到多個警告:第一喊崖,該方法通過反射侵入大部分類挣磨,而這是被禁止的雇逞;第二,Unsafe.objectFieldOffset
并不會正式給出偏移茁裙,而是一些”cookie“塘砸,可以將其傳遞給其它Unsafe
方法。[3]也就是說晤锥,這”通常有效“谣蠢,所以除非我們在做很重要的事情,否則侵入是可以的查近。一些工具眉踱,特別是 JOL,為我們完成了這些工作霜威。
在本文中谈喳,我們將會使用 JOL,因為我們想要看到 Java 對象細粒度的結(jié)構(gòu)戈泼。對于我們的需求婿禽,使用 JOL-CLI 包很合適,從這里可以獲取到:
$ wget https://repo.maven.apache.org/maven2/org/openjdk/jol/jol-cli/0.10/jol-cli-0.10-full.jar -O jol-cli.jar
$ java -jar jol-cli.jar
Usage: jol-cli.jar <mode> [optional arguments]*
Available modes:
internals: Show the object internals: field layout and default contents, object header
...
對于目標對象大猛,我們將會盡可能嘗試使用 JDK 中的類扭倾。這將會使整個事情易于驗證,因為你只需要 JOL CLI JAR 以及 JDK 來執(zhí)行測試挽绩。在更復雜的場景中膛壹,我們將會使用 JOL Samples。作為最后的手段唉堪,我們將會使用示例類模聋。
3.2. JDKs
當前最普遍的 JDK 版本還是 JDK 8。因此我們也會使用這個版本唠亚,所以本文中的結(jié)論是直接有用的链方。直到 JDK 15,字段布局策略沒有實質(zhì)性的改動灶搜,在稍后的章節(jié)將會詳細討論祟蚀。JDK 類布局自身也可能改變,所以我們?nèi)詫L試不同的 JDK 版本割卖。另外在某些時候我們將同時需要 x86_32 和 x86_64 兩個二進制文件前酿。
對我來看,可以使用我自己編譯的二進制文件:
$ curl https://builds.shipilev.net/openjdk-jdk8/openjdk-jdk8-latest-linux-x86_64-release.tar.xz | tar xJf -; mv j2sdk-image jdk8-64
$ curl https://builds.shipilev.net/openjdk-jdk8/openjdk-jdk8-latest-linux-x86-release.tar.xz | tar xJf -; mv j2sdk-image jdk8-32
$ curl https://builds.shipilev.net/openjdk-jdk/openjdk-jdk-latest-linux-x86_64-release.tar.xz | tar xJf -; mv jdk jdk15-64
$ jdk8-64/bin/java -version
openjdk version "1.8.0-builds.shipilev.net-openjdk-jdk8-b51-20200410"
OpenJDK Runtime Environment (build 1.8.0-builds.shipilev.net-openjdk-jdk8-b51-20200410-b51)
OpenJDK 64-Bit Server VM (build 25.71-b51, mixed mode)
$ jdk8-32/bin/java -version
openjdk version "1.8.0-builds.shipilev.net-openjdk-jdk8-b51-20200410"
OpenJDK Runtime Environment (build 1.8.0-builds.shipilev.net-openjdk-jdk8-b51-20200410-b51)
OpenJDK Server VM (build 25.71-b51, mixed mode)
$ jdk15-64/bin/java -version
openjdk version "15-testing" 2020-09-15
OpenJDK Runtime Environment (build 15-testing+0-builds.shipilev.net-openjdk-jdk-b1214-20200410)
OpenJDK 64-Bit Server VM (build 15-testing+0-builds.shipilev.net-openjdk-jdk-b1214-20200410, mixed mode, sharing)
4. 數(shù)據(jù)類型和它們的表示
我們需要從一些基礎知識開始究珊。每次執(zhí)行 JOL "internals"薪者,你將會看到這樣的輸出(為了簡潔起見,在之后將會省略):
$ jdk8-64/bin/java -jar jol-cli.jar internals java.lang.Object
...
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
這意味著 Java 引用占用 4 字節(jié)(壓縮引用)剿涮,boolean
/byte
占用 1 字節(jié)言津,char
/short
占用 2 字節(jié)攻人,int
/float
占用 4 字節(jié),double
/long
占用 8 字節(jié)悬槽。當做為數(shù)組元素時占用相同的空間怀吻。
為什么要搞清楚這個?因為 Java 語言規(guī)范對數(shù)據(jù)表示并沒有規(guī)定初婆,它只規(guī)定了這些類型接受什么值蓬坡。理論上可以為所有基本類型分配 8 字節(jié),只要對于它們的操作滿足規(guī)范即可磅叛。在當前的 Hotspot 中屑咳,基本上所有數(shù)據(jù)類型精確匹配值的范圍,除了 boolean
類型弊琴。例如 int
支持的值范圍為 -2147483648
至 2147483647
兆龙,精確適合 4 字節(jié)帶符號表示。
就像上面說的敲董,有一個例外紫皇,也就是 boolean
。理論上它僅僅有兩個值:true
和 false
腋寨,所以它可以用 1 比特表示聪铺。實際上 boolean
字段和數(shù)組元素仍然占用 1 個字節(jié),這有兩個原因:Java 內(nèi)存模型對于單個字段和元素保證 沒有 word tearing 問題萄窜,這就導致很難處理 1 比特字段铃剔,另外字段偏移做為內(nèi)存尋址,也就是以字節(jié)為單位脂倦,這使得尋址 boolean
字段很尷尬番宁。所以為每個 boolean
分配 1 字節(jié)是實踐上的妥協(xié)。
DDIQ: 但是無論如何要實現(xiàn) 1 比特 boolean 字段和元素的成本是什么赖阻?
大部分現(xiàn)代的硬件不支持原子訪問單個比特。對于讀操作不是問題踱蠢,我們可以讀整個字節(jié)火欧,然后 mask-shift 想要的比特。但是對于寫操作問題就大了茎截,對于相鄰
boolean
字段不能被寫覆蓋("the absence of word tearing")苇侵。換句話說,兩個線程不能執(zhí)行整個字節(jié)的寫操作:Thread 1: mov %r1, (loc) # read the entire byte or %r1, 0x01 # set the 1-st bit mov (loc), %r1 # write the byte back Thread 2: mov %r2, (loc) # read the entire byte or %r2, 0x10 # set the 2-nd bit mov (loc), %r2 # write the byte back
...因為這樣會丟失寫入的數(shù)據(jù):一個線程不會向另一個線程通知寫操作企锌,并且會覆蓋寫入的數(shù)據(jù)榆浓,這是一大禁忌。理論上來說你可以這樣實現(xiàn)原子性:
Thread 1: lock or (loc), 0x01 # set the 1-st bit in-place Thread 2: lock or (loc), 0x10 # set the 2-st bit in-place
...或者執(zhí)行 CAS 循環(huán)撕攒,這都有效陡鹃,但是這將導致一個小小的
boolean
寫操作有嚴重的性能問題烘浦。
5. Mark Word
繼續(xù)關(guān)注實際的對象結(jié)構(gòu)。讓我們以很簡單的 java.lang.Object
為例萍鲸,JOL 輸出:
$ jdk8-64/java -jar jol-cli.jar internals java.lang.Object
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
Instantiated the sample instance via default constructor.
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 00 10 00 00 # (not mark word)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
結(jié)果顯示前 12 字節(jié)是對象頭部闷叉。很不幸它沒有詳細解析頭部的內(nèi)部結(jié)構(gòu),所以我們需要深入 Hotspot 源碼脊阴。在代碼中你將會看到對象頭部包含兩部分:mark word 和 class word握侧。class word 保存對象類型信息:關(guān)聯(lián)描述類的本地結(jié)構(gòu)。下一章將會討論該部分嘿期。剩下的元數(shù)據(jù)保存在 mark word 中品擎。
mark word 有許多用處:
- 存儲移動 GC 的元數(shù)據(jù)(轉(zhuǎn)移和對象年齡)。
- 存儲身份 hash code
- 存儲鎖信息
注意每個對象都有一個 mark word备徐,因為對每個 Java 對象的處理邏輯都是一樣的孽查。這也是將對象內(nèi)部結(jié)構(gòu)起始部分做為 mark word 的原因:VM 需要在耗時敏感的邏輯中快速訪問這些信息,例如 STW GC坦喘。理解 mark word 的使用場景就能得出其占用空間的下限盲再。
5.1. 為移動 GC 存儲轉(zhuǎn)移信息
當 GC 移動對象的時候,它需要記錄對象新的位置瓣铣,至少是臨時的答朋。GC 將會使用 mark word 編碼該信息,以協(xié)調(diào)遷移和更新引用的工作棠笑。這要求 mark word 至少要與 Java 引用表示一樣長梦碗。基于 Hotspot 中壓縮引用的實現(xiàn)方式蓖救,這個引用總是未壓縮洪规,所以 mark word 至少要與機器指針一樣寬。
反過來循捺,這定義了在實現(xiàn)中 mark word 所需的最小內(nèi)存:32位平臺為 4 字節(jié)斩例,64位平臺為 8 字節(jié)。
DDIQ: 我們可以在 mark word 中記錄壓縮引用么从橘?
當然念赶,可以。但是在堆太大不能壓縮或者壓縮引用關(guān)閉的情況下仍然是個問題恰力。這可以基于運行時檢查處理叉谜,但是這樣的話在本地 GC 代碼中每次訪問對象都需要檢查,這將很不方便踩萎。通過一些工程手段也可以緩解問題停局,但是基于成本與收益權(quán)衡,仍然不建議這樣做。
DDIQ: 我們可以將 GC 轉(zhuǎn)移信息存儲在其它地方么董栽?
當然码倦,我們可以使用對象中的任一部分。然而有一個重要的問題:從 GC 的角度來看裆泳,你不僅需要知道對象轉(zhuǎn)移到哪里叹洲,也需要知道是否已經(jīng)轉(zhuǎn)移了對象。這意味著需要設定一個特定的值表示“不需要轉(zhuǎn)移”工禾,其它值解析為“轉(zhuǎn)移到 X”运提。如果我們選擇對象中任意位置,而該部分已經(jīng)存在一個看起來像“轉(zhuǎn)移到 X”的值闻葵,那么 GC 就會出問題民泵。你需要控制值的設置,以避免這樣的沖突槽畔。例如在早期的 Shenandoah 原型中栈妆,用 class word 存儲轉(zhuǎn)移信息,而這個實驗早就報廢了厢钧。最終 Shenandoah 的實現(xiàn)使用了與 STW GCs 相同的 mark word鳞尔。
你也可以像 ZGC 那樣,硬著頭皮將轉(zhuǎn)移信息完全存儲在堆之外早直。
很不幸我們不能在 Java 應用(JOL 就是一個 Java 應用)中查看包含 GC 轉(zhuǎn)移信息的 mark word寥假,因為要么在 stop-the-world GC 停頓結(jié)束后這些信息就沒有了,要么并發(fā) GC 屏障將會阻止我們看到舊對象霞扬。
5.2. 存儲 GC 的年齡信息
但是我們可以展示對象的年齡信息糕韧!
$ jdk8-32/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_19_Promotion
# Running 32-bit HotSpot VM.
Fresh object is at d2d6c0f8
*** Move 1, object is at d31104a0
(object header) 09 00 00 00 (00001001 00000000 00000000 00000000)
^^^^
*** Move 2, object is at d3398028
(object header) 11 00 00 00 (00010001 00000000 00000000 00000000)
^^^^
*** Move 3, object is at d3109688
(object header) 19 00 00 00 (00011001 00000000 00000000 00000000)
^^^^
*** Move 4, object is at d43c9250
(object header) 21 00 00 00 (00100001 00000000 00000000 00000000)
^^^^
*** Move 5, object is at d41453f0
(object header) 29 00 00 00 (00101001 00000000 00000000 00000000)
^^^^
*** Move 6, object is at d6350028
(object header) 31 00 00 00 (00110001 00000000 00000000 00000000)
^^^^
*** Move 7, object is at a760b638
(object header) 31 00 00 00 (00110001 00000000 00000000 00000000)
^^^^
留意每次移動都是如何計數(shù)的。這就是對象的年齡喻圃。在第 7 次移動后萤彩,年齡奇怪地停在了 6
。這滿足 InitialTenuringThreshold=7
的默認設置斧拍。如果你增加這個閾值雀扶,那么對象在轉(zhuǎn)移到老年代之前會經(jīng)歷更多次移動。
5.3. 身份 Hash Code
每個 Java 對象都擁有一個 hash code饮焦。如果用戶沒有定義怕吴,那么就會使用身份 hash code。[4]因為給定對象的身份 hash code 在生成后就不會改變县踢,所以需要存儲在某個地方。在 Hotspot 中伟件,它存儲在對應對象的 mark word 中硼啤。基于身份 hash code 可以接受的準確度斧账,它需要 4 個字節(jié)來存儲谴返。在上一節(jié)中已經(jīng)討論過 mark word 至少有 4 字節(jié)煞肾,所以空間是有的。
DDIQ: 如何既存儲身份 hash code 又存儲 GC 轉(zhuǎn)移信息嗓袱?
這個答案有地兒巧妙:當 GC 移動對象的時候籍救,它實際上在處理對象的兩個副本,一個在舊的位置渠抹,一個在新的位置蝙昙。新對象包含所有原始頭部信息。舊對象僅僅服務 GC 的需求梧却,因此可以用 GC 元數(shù)據(jù)覆寫頭部奇颠。Hotspot 中大部分(可能是所有)stop-the-world GCs 都這樣工作,全并發(fā)的 Shenandoah GC 也這樣工作放航。
DDIQ: 為什么我們需要存儲身份 hash code烈拒?這如何影響用戶定義的 hash code?
hash code 應該具有兩個屬性:a) 良好的分布广鳍,這意味著不同對象的值或多或少是不同的荆几; b) 冪等,這意味著具有相同關(guān)鍵對象組件的對象具有相同的哈希碼赊时。請注意后者隱含著吨铸,如果對象沒有更改那些關(guān)鍵對象組件,則其 hash code 也不會改變蛋叼。
在對象使用后更改
hashCode
焊傅,經(jīng)常會導致錯誤。例如狈涮,將對象作為鍵添加到HashMap
中狐胎,然后修改其字段,以至于hashCode
也發(fā)生變化歌馍,這將導致令人驚訝的現(xiàn)象:該對象可能在 map 中根本找不到握巢,因為內(nèi)部實現(xiàn)將會查找“錯誤”的桶。同樣松却,hash code 分布不均勻也會導致性能問題暴浦,例如返回一個常數(shù)。對于用戶指定的 hash code晓锻,通過計算用戶選擇的字段歌焦,以滿足上述兩個屬性⊙舛撸基于足夠多的字段和字段值独撇,它就可以很好地分布,并且通過計算未更改(例如
final
)的字段,就可以冪等纷铣。在這種情況下卵史,我們不需要將 hash code 存儲在任何地方。一些 hash code 實現(xiàn)可能選擇將其緩存在另一個字段中搜立,但這不是必需的以躯。對于身份 hash code,無法保證存在用于計算 hash code 的字段啄踊,即使我們有一些字段忧设,也無法得知這些字段是否穩(wěn)定∩缤矗考慮沒有字段的
java.lang.Object
:它的 hash code 是什么见转?分配的兩個Object
幾乎就是互為鏡像:它們具有相同的元數(shù)據(jù),它們具有相同的(也就是空的)內(nèi)容蒜哀。關(guān)于它們的唯一區(qū)別是分配的地址斩箫,但是即使那樣,仍然有兩個麻煩撵儿。首先乘客,地址的熵很低,尤其是像大多數(shù)Java GC 所采用 bump-ptr 分配器淀歇,地址分布不均易核。其次,GC 移動 對象浪默,因此地址不是冪等的牡直。從性能的角度來看,返回常數(shù)是不可行的纳决。因此碰逸,當前的實現(xiàn)從內(nèi)部 PRNG(“分布良好”)計算身份 hash code,并為每個對象存儲(“冪等”)阔加。
由身份 hash code 引起的 markword 改變饵史,可以通過 JOLSample_15_IdentityHashCode 觀察到。以 64位 VM 運行:
$ jdk8-64/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_15_IdentityHashCode
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
**** Fresh object
org.openjdk.jol.samples.JOLSample_15_IdentityHashCode$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 88 55 0d 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
hashCode: 5ccddd20
**** After identityHashCode()
org.openjdk.jol.samples.JOLSample_15_IdentityHashCode$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 20 dd cd
4 4 (object header) 5c 00 00 00
8 4 (object header) 88 55 0d 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
注意 hash code 為 5ccddd20
胜榔。你可以從對象頭部觀察到:01 20 dd cd 5c
胳喷。01
是 mark word 標簽,接下來是小端編寫的身份 hash code夭织。我們?nèi)匀贿€有 3 字節(jié)空閑吭露!由于我們有相對大的 mark word,所以這是可能的尊惰。如果在 mark word 僅有 4 字節(jié)的 32 位 VM 上運行會怎樣呢奴饮?
這是結(jié)果:
$ jdk8-32/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_15_IdentityHashCode
# Running 32-bit HotSpot VM.
**** Fresh object
org.openjdk.jol.samples.JOLSample_15_IdentityHashCode$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) c0 ab 6b a3
Instance size: 8 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
hashCode: 12ddf17
**** After identityHashCode()
org.openjdk.jol.samples.JOLSample_15_IdentityHashCode$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 81 8b ef 96
4 4 (object header) c0 ab 6b a3
Instance size: 8 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
很明顯對象頭部改變了纬向。但是需要敏銳的眼睛才能看到 12ddf17
實際在哪里择浊。你在頭部看到的是身份 hashcode “向右移動了一位”戴卜。所以第一個字節(jié)結(jié)尾的一比特,輸出了 81
琢岩,剩下的轉(zhuǎn)化為 12ddf17 >> 1 = 96ef8b
投剥。注意,這將身份 hash code 的值范圍由 32 比特縮小到了“僅僅” 25 比特担孔。
DDIQ: 但是等一下江锨,
System.identityHashCode
返回int
值,所以這是完整的 32 比特 hashcode 么糕篇?
identityHashCode
的范圍沒有明確指定啄育,就是為了實現(xiàn)這種權(quán)衡。在 32 位模式下設置整個 32 比特身份 hash code 需要為每個對象添加一個 word拌消,這對內(nèi)存占用來說是一個問題挑豌。該實現(xiàn)允許縮減 hashcode 以適用存儲位數(shù)唆垃。很不幸這又導致了比較看似相同的 Java 代碼在 32 位和 64 位執(zhí)行時的極端狀況蕊玷。
5.4. 鎖信息
Java 同步采用了一個復雜的狀態(tài)機。由于每個 Java 對象都可以被同步捶闸,所以鎖狀態(tài)應該關(guān)聯(lián)到任一 Java 對象鹦筹。mark word 保存了大部分狀態(tài)铝阐。
這些鎖轉(zhuǎn)換的不同部分可以在對象頭部看到。例如铐拐,當一個 Java 鎖偏向某個對象時徘键,我們需要記錄對象附近鎖的信息。這可以通過 JOLSample_13_BiasedLocking 觀察:
$ jdk8-64/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_13_BiasedLocking
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
**** Fresh object
org.openjdk.jol.samples.JOLSample_13_BiasedLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 # No lock
4 4 (object header) 00 00 00 00
8 4 (object header) c0 07 08 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the lock
org.openjdk.jol.samples.JOLSample_13_BiasedLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 b0 00 80 # Biased lock
4 4 (object header) b8 7f 00 00 # Biased lock
8 4 (object header) c0 07 08 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the lock
org.openjdk.jol.samples.JOLSample_13_BiasedLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 b0 00 80 # Biased lock
4 4 (object header) b8 7f 00 00 # Biased lock
8 4 (object header) c0 07 08 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
注意我們?nèi)绾卧陬^部記錄對鎖描述符的本地指針:b0 00 80 b8 7f
遍蟋。這個鎖偏向了這個對象吹害。
當鎖沒有偏向的時候,情況類似匿值,看例子 JOLSample_14_FatLocking:
$ jdk8-64/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_14_FatLocking
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
**** Fresh object
org.openjdk.jol.samples.JOLSample_14_FatLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # No lock
4 4 (object header) 00 00 00 00
8 4 (object header) c0 07 08 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** Before the lock
org.openjdk.jol.samples.JOLSample_14_FatLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 78 19 57 1a # Lightweight lock
4 4 (object header) 85 7f 00 00
8 4 (object header) c0 07 08 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the lock
org.openjdk.jol.samples.JOLSample_14_FatLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a 4b 00 b4 # Heavyweight lock
4 4 (object header) 84 7f 00 00
8 4 (object header) c0 07 08 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the lock
org.openjdk.jol.samples.JOLSample_14_FatLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 0a 4b 00 b4 # Heavyweight lock
4 4 (object header) 84 7f 00 00
8 4 (object header) c0 07 08 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After System.gc()
org.openjdk.jol.samples.JOLSample_14_FatLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 09 00 00 00 # Lock recycled
4 4 (object header) 00 00 00 00
8 4 (object header) c0 07 08 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
在這里我們看到了鎖通常的生命周期:首先對象沒有鎖記錄赠制,然后其它線程獲取了鎖,設置為(輕量)同步鎖挟憔,然后主線程參與競爭钟些,鎖就膨脹了,在解鎖后鎖信息仍然指向膨脹鎖绊谭。最后在某個時刻鎖收縮了政恍,對象釋放了相關(guān)的鎖。
5.5. 觀察: 身份 Hashcode 使得偏向鎖失效
當偏向鎖生效的時候需要保存身份 hashcode 會怎樣达传?很簡單:身份 hashcode 優(yōu)先篙耗,偏向鎖失效迫筑。在例子 JOLSample_26_IHC_BL_Conflict 中可以看到:
$ jdk8-64/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
**** Fresh object
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 # No lock
4 4 (object header) 00 00 00 00
8 4 (object header) f8 00 01 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the lock
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 b0 00 20 # Biased lock
4 4 (object header) e5 7f 00 00 # Biased lock
8 4 (object header) f8 00 01 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the lock
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 b0 00 20 # Biased lock
4 4 (object header) e5 7f 00 00 # Biased lock
8 4 (object header) f8 00 01 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
hashCode: 65ae6ba4
**** After the hashcode
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 a4 6b ae # Hashcode
4 4 (object header) 65 00 00 00 # Hashcode
8 4 (object header) f8 00 01 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the second lock
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 50 f9 b8 29 # Lightweight lock
4 4 (object header) e5 7f 00 00 # Lightweight lock
8 4 (object header) f8 00 01 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the second lock
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 a4 6b ae # Hashcode
4 4 (object header) 65 00 00 00 # Hashcode
8 4 (object header) f8 00 01 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
在這個例子中,對于新對象偏向鎖生效宗弯,但是當我們獲取它的 hashCode
脯燃,最終計算它的身份 hash code(由于沒有重寫 Object.hashCode
),并且將計算的值設置在 mark word蒙保。接下來的鎖僅能暫時替換身份 hash code辕棚,但是一旦(非偏向)鎖釋放,它又會回來邓厕。由于再也不能將偏向鎖信息保存在 mark word 中了逝嚎,所以偏向鎖對這個對象失效了。
DDIQ: 這種沖突僅影響一個實例么详恼?
未必补君。根本問題是解偏向(unbias)的成本很高,所以偏向鎖的機制將會盡可能最小化重新偏向的頻率昧互。如果該機制檢查到某些解偏向很頻繁挽铁,它可能會決定整類對象在接下來都應該重新偏向,或者不能偏向硅堆。
5.6. 觀察: 32 位 VMs 改善內(nèi)存占用
由于 mark word 依賴對應的位數(shù)屿储,可想而知 32 位 VM 的對象占用更少空間,即使不包含涉及的(引用)字段渐逃。這可以通過 32 位和 64 位 VM 中 Object
的布局展示:
$ jdk8-64/bin/java -jar jol-cli.jar internals java.lang.Object
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
Instantiated the sample instance via default constructor.
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 00 10 00 00 # Class word (compressed)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
$ jdk8-32/bin/java -jar jol-cli.jar internals java.lang.Object
# Running 32-bit HotSpot VM.
Instantiated the sample instance via default constructor.
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 48 51 2b a3 # Class word
Instance size: 8 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
64 位 VM 的 mark word 占用 8 (mark word) + 4 (class word) = 12 字節(jié)够掠,相對比 32 位 VM 占用 4 (mark word) + 4 (class word) = 8 字節(jié)。由于對象以 8 字節(jié)對齊茄菊,所以向上舍入為 16 和 8 字節(jié)疯潭。對于這個小對象來說,空間節(jié)省 2x面殖!
6. Class Word
從本地機器來看竖哩,每個對象就是一堆字節(jié)。存在這樣的場景脊僚,在運行時我們想知道處理的對象的類型相叁。下面是一個不完整的場景列表:
- 運行時類型檢查。
- 判斷對象大小辽幌。
- 指定虛調(diào)用和接口調(diào)用的目標增淹。
class word 也是被壓縮的。即使類指針不是 Java 堆引用乌企,也可以采用類似的優(yōu)化虑润。[5]
6.1. 運行時類型檢查
Java 是一個類型安全的語言,所以它需要運行時類型檢查加酵。class word 保存對象的類型信息拳喻,使得編譯器可以進行運行時類型檢查哭当。運行時檢查的效率依賴類型元數(shù)據(jù)的結(jié)構(gòu)。
如果元數(shù)據(jù)編碼為一個簡單的表單冗澈,那么編譯器可以直接內(nèi)聯(lián)這些檢查钦勘。在 Hotspot 中,class word 保存指向 VM Klass
的本地指針 渗柿,其中保存了一些元信息个盆,包括繼承的父類類型,實現(xiàn)的接口朵栖,等。class word 也保存了 Java mirror柴梆,也就是 java.lang.Class
的關(guān)聯(lián)實例陨溅。這種迂回的實現(xiàn)方式使得 java.lang.Class
可以被當做普通的對象看待,在 GC 的時候移動它們不需要更新每個對象的 class word:java.lang.Class
可以移動绍在,但是 Klass
一直保持在原有位置门扇。
DDIQ: 所以,類型檢查成本很高?
在很多情況下偿渡,類型或多或少可以從上下文精確獲取臼寄。例如,對于接受
MyClass
參數(shù)的方法溜宽,我們可以確定參數(shù)是MyClass
或其子類吉拳。所以通常不需要類型檢查。但是如果失敗了适揉,那么我們需要訪問對象元數(shù)據(jù)進行運行時檢查留攒。例如devirtualization 和檢查類型轉(zhuǎn)換。例如這樣的檢查類型轉(zhuǎn)換:
private Object o = new MyClass(); @CompilerControl(CompilerControl.Mode.DONT_INLINE) @Benchmark public MyClass testMethod() { return (MyClass)o; }
mov 0x10(%rsi),%rax ; getfield "o" mov 0x8(%rax),%r10 ; get o.<classword>, Klass* movabs $0x7f5bc5144c48,%r11 ; load known Klass* for MyClass cmp %r11,%r10 ; checked cast jne 0x00007f64004e1b63 ; not equal? go to slowpath, check subclasses there ... %rax is definitely MyClass now
DDIQ: 所以嫉嘀,可以基于該結(jié)構(gòu)進行優(yōu)化(intrinics)么炼邀?
是的,實際上剪侮,
Object.getClass()
將會被這樣優(yōu)化:@CompilerControl(CompilerControl.Mode.DONT_INLINE) @Benchmark public Class<?> test() { return o.getClass(); }
mov 0x10(%rsi),%r10 ; getfield "o" mov 0x8(%r10),%r10 ; get o.<classword>, Klass* mov 0x70(%r10),%r10 ; get Klass._java_mirror, OopHandle mov (%r10),%rax ; dereference OopHandle, get java.lang.Class ... %rax is now java.lang.Class instance
6.2. 判斷對象大小
確定對象大小采用類似的方法拭宁。相對于不知道對象類型的運行時類型檢查,分配過程中或多或少更確定分配對象的大邪旮:可以通過使用的構(gòu)造器類型和數(shù)組初始化器等確定杰标。所以在這個場景下不需要訪問 classword。
但是在一些本地代碼(最著名的就是垃圾回收器)中降铸,可能會像這樣遍歷可解析的堆內(nèi)存:
HeapWord* cur = heap_start;
while (cur < heap_used) {
object o = (object)cur;
do_object(o);
cur = cur + o->size();
}
對于這樣場景在旱,本地代碼需要知道當前(沒有類型的!)對象的大小推掸,而且希望高效的獲取桶蝎。所以對于本地代碼來說驻仅,類元數(shù)據(jù)的組織很重要。在 Hotspot 中登渣,我們可以通過 layout helper 訪問 class word噪服,這樣就能為我們提供對象大小信息。
DDIQ: 你說垃圾收集器使用了堆外的內(nèi)存胜茧?
是的粘优,Hotspot GCs 需要訪問類元數(shù)據(jù)以獲取對象大小。大部分情況下將會反復獲取同一個元數(shù)據(jù)呻顽,但是相關(guān)的內(nèi)存讀操作仍然有一定的成本雹顺。The wonders of untyped native accesses! 你可以從本地代碼的反匯編中看到這一點,例如
MutableSpace::object_iterate
這里:$ objdump -lrdSC ./build/linux-x86_64-server-release/hotspot/variant->server/libjvm/objs/mutableSpace.o ... void MutableSpace::object_iterate(ObjectClosure* cl) { ... # HeapWord* p = bottom(); ... # while (p < top()) { ... # Klass* oopDesc::klass() const { # if (UseCompressedClassPointers) { # return >CompressedKlassPointers::decode_not_null(_metadata._compressed_klass); # } else { # return _metadata._klass; ... d0: 49 8b 7e 08 mov 0x8(%r14),%rdi ; get Klass* # int layout_helper() const { return _layout_helper; } d4: 8b 4f 08 mov 0x8(%rdi),%ecx ; get layout helper # if (lh > Klass::_lh_neutral_value) { d7: 83 f9 00 cmp $0x0,%ecx da: 7e 4e jle 12a # if (!Klass::layout_helper_needs_slow_path(lh)) { dc: f6 c1 01 test $0x1,%cl ; layout helper *is* size? df: 0f 85 9b 00 00 00 jne 180 # s = lh >> LogHeapWordSize; // deliver size scaled by wordSize e5: 89 c8 mov %ecx,%eax e7: c1 f8 03 sar $0x3,%eax ; this is object size now # p += oop(p)->size(); ea: 48 98 cltq ec: 4d 8d 34 c6 lea (%r14,%rax,8),%r14 f0: 49 8b 44 24 38 mov 0x38(%r12),%rax # while (p < top()) { ... # cl->do_object(oop(p)); ... 103: ff 10 callq *(%rax)
6.3. 指定虛調(diào)用和接口調(diào)用的目標
當運行時系統(tǒng)想要調(diào)用對象實例的虛方法和接口方法的時候廊遍,它需要確定目標方法在哪里嬉愧。雖然大部分場景可以被優(yōu)化,但是仍然有需要做分發(fā)的場景喉前。分發(fā)的性能也依賴類元數(shù)據(jù)的獲取没酣,所以這不能被忽略。
6.4. 觀察: 壓縮引用影響對象頭部的內(nèi)存占用
與 JVM 位數(shù)影響 mark word 大小類似卵迂,壓縮引用的模式可以會影響對象大小裕便,即使是不考慮引用字段的情況下。為了展示這個問題见咒,讓我們分別在谐ニァ(1GB)大(64GB)兩種堆內(nèi)存下測試 java.lang.Integer
。默認情況下小堆的壓縮引用會打開论颅,大堆的會關(guān)閉哎垦。這也意味著壓縮類指針也會對應打開和關(guān)閉。
$ jdk8-64/bin/java -Xmx1g -jar jol-cli.jar internals java.lang.Integer
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
Instantiated the sample instance via public java.lang.Integer(int)
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) de 21 00 20 # Class word
12 4 int Integer.value 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
$ jdk8-64/bin/java -Xmx64g -jar jol-cli.jar internals java.lang.Integer
# Running 64-bit HotSpot VM.
Instantiated the sample instance via public java.lang.Integer(int)
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 40 69 25 ad # Class word
12 4 (object header) e5 7f 00 00 # (uncompressed)
16 4 int Integer.value 0
20 4 (loss due to the next object alignment)
Instance size: 24 bytes # AHHHHHHH....
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
在 1GB 堆內(nèi)存的 VM 中恃疯,對象頭部占用 8 (mark word) + 4 (class word) = 12 字節(jié)漏设,對應的 64GB VM 占用 8 (mark word) and + 8 (class word) = 16 字節(jié)。如果沒有字段今妄,由于對象以 8 字節(jié)對齊郑口,那么兩者都會向上舍入至 16 字節(jié),但是在測試中存在一個 int
字段盾鳞,所以在 64GB 的情況下犬性,在 16 字節(jié)的頭部之后需要再分配 8 字節(jié),總共占用 24 字節(jié)腾仅。
7. 頭部:數(shù)值長度
數(shù)組還需要另外一種元數(shù)據(jù):數(shù)組長度乒裆。由于對象類型僅僅編碼數(shù)組元素類型,我們需要在另外一個地方存儲數(shù)組長度推励。
可以從 JOLSample_25_ArrayAlignment 觀察到:
$ jdk8-64/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_25_ArrayAlignment
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
[J object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) d8 0c 00 00 # Class word
12 4 (object header) 00 00 00 00 # Array length
16 0 long [J.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
...
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 68 07 00 00 # Class word
12 4 (object header) 00 00 00 00 # Array length
16 0 byte [B.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 68 07 00 00 # Class word
12 4 (object header) 01 00 00 00 # Array length
16 1 byte [B.<elements> N/A
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 68 07 00 00 # Class word
12 4 (object header) 02 00 00 00 # Array length
16 2 byte [B.<elements> N/A
18 6 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 6 bytes external = 6 bytes total
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 68 07 00 00 # Class word
12 4 (object header) 03 00 00 00 # Array length
16 3 byte [B.<elements> N/A
19 5 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 5 bytes external = 5 bytes total
...
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 68 07 00 00 # Class word
12 4 (object header) 08 00 00 00 # Array length
16 8 byte [B.<elements> N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
在 +12 的位置保存的就是數(shù)組長度鹤耍。隨著我們分配 0..8 個元素的 byte[]
數(shù)組肉迫,該位置的數(shù)據(jù)也相應的變化。在數(shù)組實例中保存數(shù)組長度有利于對象遍歷時計算對象大懈寤啤(前一節(jié)討論過普通對象)喊衫,另外也有利于高效進行范圍檢查。
DDIQ: 向我們展示一下如何進行數(shù)組范圍檢查杆怕?
在很多場景中族购,范圍檢查可以被消除,比如說在熱循環(huán)中陵珍,但是對于數(shù)組未知的情況:
private int[] a = new int[100]; @CompilerControl(CompilerControl.Mode.DONT_INLINE) @Benchmark public int test() { return a[42]; }
mov 0x10(%rsi),%r10 ; get field "a" mov 0x10(%r10),%r11d ; get a.<arraylength>, at 0x10 cmp $0x2a,%r11d ; compare 42 with arraylength jbe 0x00007f139b4398e1 ; equal or greater? jump to slowpath mov 0xc0(%r10),%eax ; read element at (24 + 4*42) = 0xc0
7.1. 觀察:數(shù)組起始位置是對齊的
上面的例子掩蓋了數(shù)組布局中重要的問題寝杖,被 64位模式下的對齊隱藏了。如果我們以較大的堆運行(或者顯式關(guān)閉壓縮引用)來干擾對齊方式:
$ jdk8-64/bin/java -Xmx64g -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_25_ArrayAlignment
# Running 64-bit HotSpot VM.
[J object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) d8 8c b0 a4 # Class word
12 4 (object header) 98 7f 00 00 # Class word
16 4 (object header) 00 00 00 00 # Array length
20 4 (alignment/padding gap)
24 0 long [J.<elements> N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
...
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 68 87 b0 a4 # Class word
12 4 (object header) 98 7f 00 00 # Class word
16 4 (object header) 05 00 00 00 # Array length
20 4 (alignment/padding gap)
24 5 byte [B.<elements> N/A
29 3 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 4 bytes internal + 3 bytes external = 7 bytes total
...
…?或者以 32位運行:
$ jdk8-32/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_25_ArrayAlignment
# Running 32-bit HotSpot VM.
[J object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 88 47 1b a3 # Class word
8 4 (object header) 00 00 00 00 # Array length
12 4 (alignment/padding gap)
16 0 long [J.<elements> N/A
Instance size: 16 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 58 44 1b a3 # Class word
8 4 (object header) 05 00 00 00 # Array length
12 5 byte [B.<elements> N/A
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
基于實現(xiàn)上的問題撑教,array base以機器字長對齊朝墩。如果數(shù)組的元素比機器字長大,那么也會盡可能對齊伟姐,我們稍后將會詳細討論字段對齊。這意味著數(shù)組可能比我們想當然的占用更多空間亿卤。
8. 對象對齊
到目前為止愤兵,我們暫且忽略了對象對齊的需求,里說當然的聲稱以 8 字節(jié)對齊排吴。那么為什么是 8 字節(jié)呢秆乳?
有許多因素是的 8 字節(jié)對齊很合理。
第一钻哩,有時候我們需要原子性更新 mark word屹堰,這就為 mark word 的位置增加了限制。對于需要完整更新的 8 字節(jié) mark word —— 例如設置移動指針 —— 就是需要以 8 字節(jié)對齊街氢。由于 mark word 位于對象開頭扯键,所以整個對象也應該以 8 字節(jié)對齊。
DDIQ: 我們可以在 32 位平臺讓對象以 4 字節(jié)對齊么珊肃?
就 mark word 而言荣刑,可以。但是這并不是我們唯一需要考慮的因素伦乔,請看下面的討論厉亏。
第二,對 volatile long/double 的原子性訪問也一樣烈和,必須對它們進行不可分割的讀寫爱只。即使沒有 volatile 修飾詞,也可能由于使用場景需要進行原子性訪問招刹,例如通過 VarHandles
恬试。因此窝趣,我們最好接受每個字段必須自然對齊。如果我們在外部以 8 對齊對象忘渔,那么在內(nèi)部以 8/4/2 對齊字段都不會打破絕對對齊高帖。
DDIQ: 這意味著我們可以查看對象字段的定義,然后決定對象應該采取哪種對齊畦粮?
是的散址,從技術(shù)上來講可以。如果我們解決了 mark word 的對齊問題宣赔,并且只有 4 字節(jié)的字段预麸,那么我們可以以 4 字節(jié)對齊對象。然后這會使分配邏輯很復雜:他需要立即決定是否需要更大的對齊(可以靜態(tài)執(zhí)行儒将,由于分配的類型已知)吏祸,是否需要增加外部的填充(需要動態(tài)檢查,因為這取決于前面的對象)钩蚊。這也為堆解析引入了問題贡翘。
以 8 字節(jié)對齊并不總是一種浪費,因為這使得超過 4GB 的堆可以進行壓縮引用砰逻。以 4 字節(jié)對齊“僅僅”可以使 16GB 的堆進行壓縮引用鸣驱,而 8 字節(jié)對齊就可以使 32GB 的堆進行壓縮引用。實際上為了擴展壓縮引用生效的范圍蝠咆,可以增加對象對齊至 16 字節(jié)踊东。
在 Hotspot 中,從技術(shù)上來說對齊時對象自身的一部分:如果我們將所有對象大小舍入至 8刚操,那么自然會在某些對象的末端出現(xiàn)對齊陰影闸翅。分配大小是 8 的倍數(shù)的對象不會打破對齊,所以如果我們從正確的起始位置開始(是的菊霜,我們可以)坚冀,那么所有對象都可以保證是對齊的。
讓我們以 java.util.ArrayList
為例:
$ jdk8-64/bin/java -Xmx1g -XX:ObjectAlignmentInBytes=16 -jar jol-cli.jar internals java.util.ArrayList
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
java.util.ArrayList object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 46 2e 00 20
12 4 int AbstractList.modCount 0
16 4 int ArrayList.size 0
20 4 java.lang.Object[] ArrayList.elementData []
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
…以 ? -XX:ObjectAlignmentInBytes=16
執(zhí)行:
$ jdk8-64/bin/java -Xmx1g -XX:ObjectAlignmentInBytes=16 -jar jol-cli.jar internals java.util.ArrayList
# Running 64-bit HotSpot VM.
# Using compressed oop with 4-bit shift.
# Using compressed klass with 4-bit shift.
# Objects are 16 bytes aligned.
Instantiated the sample instance via default constructor.
java.util.ArrayList object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 93 2e 00 20
12 4 int AbstractList.modCount 0
16 4 int ArrayList.size 0
20 4 java.lang.Object[] ArrayList.elementData []
24 8 (loss due to the next object alignment)
Instance size: 32 bytes
以 8 字節(jié)對齊占卧,ArrayList
占用 24 字節(jié)遗菠,因為對象大小是 8 的倍數(shù)。以 16 字節(jié)對齊华蜒,我們看到了對齊陰影:對象末尾丟失了 8 字節(jié)以維護下一個對象的對齊辙纬。
8.1. 觀察: 對齊陰影中的隱藏字段
這種觀察立即引出了一個明確的觀察:如果某個對象存在對齊陰影,那么我們可以在其中隱藏新字段叭喜,而且不會增加對象的大小贺拣!
比較 java.lang.Object
:
$ jdk8-64/bin/java -jar jol-cli.jar internals java.lang.Object
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) a8 0e 00 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
…?與 java.lang.Integer
:
$ jdk8-64/bin/java -jar jol-cli.jar internals java.lang.Integer
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via public java.lang.Integer(int)
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) f0 0e 01 00
12 4 int Integer.value 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
Object
有一個 4 字節(jié)的對齊陰影,Integer.value
字段欣然占用了該部分。最后譬涡,Object
和 Integer
的大小在該 VM 配置下是相同的闪幽。
8.2. 觀察: 添加小字段就能讓實例大小顯著增加
這個故事有相反的告誡。假如對象沒有對齊陰影:
public class A {
int a1;
}
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . A
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 28 b8 0f 00
12 4 int A.a1 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
如果添加一個 boolean
字段會怎樣呢涡匀?
public class B {
int b1;
boolean b2; // takes 1 byte, right?
}
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . B
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 28 b8 0f 00
12 4 int B.b1 0
16 1 boolean B.b2 false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
在這里盯腌,我們只需要一個糟糕的字節(jié)來分配字段,但是因為我們需要滿足對象對齊的要求陨瘩,我們最終增加了整整 8 字節(jié)腕够!有一個小小的安慰,在陰影中剩下的 7 字節(jié)添加更多字段將不會增加表面上的對象大小舌劳。
9. 字段對齊
在討論對象對齊的時候帚湘,我們已經(jīng)在前面的章節(jié)討論了該主題。
很多架構(gòu)不喜歡未對齊的訪問甚淡。在很多情況下大诸,未對齊訪問將會帶來性能損失。在有些情況下贯卦,未對齊訪問將會引發(fā)機器異常资柔。然后 Java 內(nèi)存模型來了,它要求對字段和數(shù)組元素進行原子訪問撵割,至少是在字段聲明為 volatile
的情況下建邓。
這迫使大多數(shù)實現(xiàn)將字段對齊為自然對齊。對象以 8 字節(jié)對齊睁枕,這就保證了起始偏移 0 是以 8 字節(jié)對齊的,這是所有類型中最大的自然對齊沸手。所以我們“僅僅”需要以自然對齊布局對象中的字段即可外遇。這可以在 java.lang.Long
中清楚的看到:
$ jdk8-64/bin/java -jar jol-cli.jar internals java.lang.Long
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via public java.lang.Long(long)
java.lang.Long object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 18 11 01 00
12 4 (alignment/padding gap)
16 8 long Long.value 0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
在這里 long value
放在 +16 的位置,這樣就能使它以 8 對齊契吉。注意在這個字段之前有一個空白跳仿!
9.1. 觀察: 字段對齊空白中的隱藏字段
預告一下字段打包的討論:由于存在這些字段對齊空白,所以可以在其中隱藏字段捐晶。例如菲语,可以在包含 long
類中添加另外一個 int
字段:
public class LongIntCarrier {
long value;
int somethingElse;
}
…?最終對象布局是這樣:
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . LongIntCarrier
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
LongIntCarrier object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 28 b8 0f 00
12 4 int LongIntCarrier.somethingElse 0
16 8 long LongIntCarrier.value 0
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
對比 java.lang.Long
的布局:它們占用了同樣多的實例空間,因為新的的 int
字段使用了對齊空白惑灵。
10. 字段打包
存在多個字段時山上,將會出現(xiàn)一個新問題:如何在對象中分布字段?這就產(chǎn)生了字段布局器英支。字段布局器使得每個字段以自然對齊分配佩憾,并且盡可能地密實打包。如何準確的實現(xiàn)這一目標很大程度上取決于實現(xiàn)。我們都知道妄帘,字段“打包器”可以以聲明的順序放置字段楞黄,然后以每個字段的自然對齊填充。當然這會浪費很多內(nèi)存抡驼。
考慮這個類:
public class FieldPacking {
boolean b;
long l;
char c;
int i;
}
幼稚的字段打包器可以這樣做:
$ <32-bit simulation>
FieldPacking object internals:
OFFSET SIZE TYPE DESCRIPTION
0 4 (object header)
4 4 (object header)
8 1 boolean FieldPacking.b
9 7 (alignment/padding gap)
16 8 long FieldPacking.l
24 2 char FieldPacking.c
26 2 (alignment/padding gap)
28 4 int FieldPacking.i
Instance size: 32 bytes
…?然后聰明的打包器將會這樣做:
$ jdk8-32/bin/java -jar jol-cli.jar internals -cp . FieldPacking
# Running 32-bit HotSpot VM.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
FieldPacking object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 68 91 6f a3
8 8 long FieldPacking.l 0
16 4 int FieldPacking.i 0
20 2 char FieldPacking.c
22 1 boolean FieldPacking.b false
23 1 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 1 bytes external = 1 bytes total
…? 這樣每個對象實例就節(jié)省了 8 字節(jié)鬼廓。
DDIQ: 字段布局有經(jīng)驗法則么?
如上所述致盟,字段布局是實現(xiàn)細節(jié)碎税。直到最近,Hotspot 的實現(xiàn)還是一個幾乎線性的實現(xiàn)勾邦。它從大到小布局字段蚣录。首先布局 longs/double(需要以 8 對齊),然后是 ints/floats(需要以 4 對齊)眷篇,然后是 chars/shorts(需要以 2 對齊)萎河,最后是 bytes/booleans。以這種方式蕉饼,我們可以很緊湊的打包整個字段塊虐杯,但是有一個異常情況:較大數(shù)據(jù)類型的初始對齊可能會留下小數(shù)據(jù)類型可以占用的空白 —— 這種情況分開處理。
引用字段要么當做 8 字節(jié)字段(沒有壓縮引用的 64 位模式)處理昧港,要么當做 4 字節(jié)字段(32 位模式擎椰,或者有壓縮引用的 64 位模式)處理。當多個帶有引用字段的類是有繼承關(guān)系時创肥,有一些 GC 相關(guān)的技巧:有時將它們聚類在一起可能有好處达舒。
無論如何,我們可以從中得出兩個直接的觀察叹侄。
10.1. 觀察: 字段聲明順序 != 字段布局順序
首先巩搏,給定字段聲明順序:
public class FieldOrder {
boolean firstField;
long secondField;
char thirdField;
int fourthField;
}
…? 這并不保證在內(nèi)存中是相同的順序。字段打包器將會重新排列字段以最小化內(nèi)存占用:
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . FieldOrder
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
FieldOrder object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 28 b8 0f 00
12 4 int FieldOrder.fourthField 0
16 8 long FieldOrder.secondField 0
24 2 char FieldOrder.thirdField
26 1 boolean FieldOrder.firstField false
27 5 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 0 bytes internal + 5 bytes external = 5 bytes total
注意看布局器如何按照數(shù)據(jù)類型大小安排字段:首先 long
字段對齊在 +16趾代,然后 int
字段應該放在 +24 的問題贯底,但是布局器發(fā)現(xiàn) long
字段前面有個空白可以使用,所以它就被放在了 +12撒强,然后 char
字段按照自然對齊放在 +24禽捆,最后是 boolean 字段放在 +26。
當你想要與基于偏移量訪問字段的外部/原始函數(shù)交互時飘哨,字段打包是一個主要的問題胚想。字段的偏移量依賴字段打包器的實現(xiàn)(是否壓縮字段,具體如何處理杖玲?)顿仇,以及運行的環(huán)境條件(機器位數(shù)淘正,壓縮引用模式,對象對齊臼闻,等)鸿吆。
使用 sun.misc.Unsafe
訪問字段的 Java 代碼必須在運行時讀取字段偏移量诈嘿,以獲取運行時的實際布局绒尊。 假設這些字段與調(diào)試會話中的偏移量相同,那么就很難診斷出錯誤的來源蹦疑。
10.2. 觀察: C 樣式的填充不可靠
當涉及到 False Sharing 消除時乓搬,人們訴諸于填充關(guān)鍵字段思犁,以實現(xiàn)隔離在自身緩存行的目的。最常見的方法是在被保護字段周圍添加一些虛字段聲明进肯。而且由于寫入這些聲明很乏味激蹲,所以人們傾向于使用較大的數(shù)據(jù)類型。所以為了保護被爭用的 byte
字段江掩,你會看到這種寫法:
public class LongPadding {
long l01, l02, l03, l04, l05, l06, l07, l08; // 64 bytes
byte pleaseHelpMe;
long l11, l12, l13, l14, l15, l16, l17, l18; // 64 bytes
}
你可能期待 pleaseHelpMe
字段被兩個 long
字段塊包圍学辱。很不幸,字段打包器不這么想:
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . CStylePadding
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
LongPadding object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 28 b8 0f 00
12 1 byte LongPadding.pleaseHelpMe 0 # WHOOPS.
13 3 (alignment/padding gap)
16 8 long LongPadding.l01 0
24 8 long LongPadding.l02 0
32 8 long LongPadding.l03 0
40 8 long LongPadding.l04 0
48 8 long LongPadding.l05 0
56 8 long LongPadding.l06 0
64 8 long LongPadding.l07 0
72 8 long LongPadding.l08 0
80 8 long LongPadding.l11 0
88 8 long LongPadding.l12 0
96 8 long LongPadding.l13 0
104 8 long LongPadding.l14 0
112 8 long LongPadding.l15 0
120 8 long LongPadding.l16 0
128 8 long LongPadding.l17 0
136 8 long LongPadding.l18 0
Instance size: 144 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total
使用 byte
字段填充會怎樣呢环形?這取決于實現(xiàn)細節(jié)策泣,字段打包器以聲明順序處理相同大小/類型的字段,但至少它會起作用:
public class BytePadding {
byte p000, p001, p002, p003, p004, p005, p006, p007;
byte p008, p009, p010, p011, p012, p013, p014, p015;
byte PRNG, p017, p018, p019, p020, p021, p022, p023;
byte p024, p025, p026, p027, p028, p029, p030, p031;
byte p032, p033, p034, p035, p036, p037, p038, p039;
byte p040, p041, p042, p043, p044, p045, p046, p047;
byte p048, p049, p050, p051, p052, p053, p054, p055;
byte p056, p057, p058, p059, p060, p061, p062, p063;
byte pleaseHelpMe;
byte p100, p101, p102, p103, p104, p105, p106, p107;
byte p108, p109, p110, p111, p112, p113, p114, p115;
byte p116, p117, p118, p119, p120, p121, p122, p123;
byte p124, p125, p126, p127, p128, p129, p130, p131;
byte p132, p133, p134, p135, p136, p137, p138, p139;
byte p140, p141, p142, p143, p144, p145, p146, p147;
byte p148, p149, p150, p151, p152, p153, p154, p155;
byte p156, p157, p158, p159, p160, p161, p162, p163;
}
$ jdk8-64/bin/java -jar ~/projects/jol/jol-cli/target/jol-cli.jar internals -cp . BytePadding
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
BytePadding object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 28 b8 0f 00
12 1 byte BytePadding.p000 0
13 1 byte BytePadding.p001 0
...
74 1 byte BytePadding.p062 0
75 1 byte BytePadding.p063 0
76 1 byte BytePadding.pleaseHelpMe 0 # Good
77 1 byte BytePadding.p100 0
78 1 byte BytePadding.p101 0
...
139 1 byte BytePadding.p162 0
140 1 byte BytePadding.p163 0
141 3 (loss due to the next object alignment)
Instance size: 144 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
…? 除非你需要保護一個不同類型的字段:
public class BytePaddingHetero {
byte p000, p001, p002, p003, p004, p005, p006, p007;
byte p008, p009, p010, p011, p012, p013, p014, p015;
byte p016, p017, p018, p019, p020, p021, p022, p023;
byte p024, p025, p026, p027, p028, p029, p030, p031;
byte p032, p033, p034, p035, p036, p037, p038, p039;
byte p040, p041, p042, p043, p044, p045, p046, p047;
byte p048, p049, p050, p051, p052, p053, p054, p055;
byte p056, p057, p058, p059, p060, p061, p062, p063;
byte pleaseHelpMe;
int pleaseHelpMeToo; // pretty please!
byte p100, p101, p102, p103, p104, p105, p106, p107;
byte p108, p109, p110, p111, p112, p113, p114, p115;
byte p116, p117, p118, p119, p120, p121, p122, p123;
byte p124, p125, p126, p127, p128, p129, p130, p131;
byte p132, p133, p134, p135, p136, p137, p138, p139;
byte p140, p141, p142, p143, p144, p145, p146, p147;
byte p148, p149, p150, p151, p152, p153, p154, p155;
byte p156, p157, p158, p159, p160, p161, p162, p163;
}
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . BytePaddingHetero
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
BytePaddingHetero object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 28 b8 0f 00
12 4 int BytePaddingHetero.pleaseHelpMeToo 0 # WHOOPS.
16 1 byte BytePaddingHetero.p000 0
17 1 byte BytePaddingHetero.p001 0
...
78 1 byte BytePaddingHetero.p062 0
79 1 byte BytePaddingHetero.p063 0
80 1 byte BytePaddingHetero.pleaseHelpMe 0 # Good.
81 1 byte BytePaddingHetero.p100 0
82 1 byte BytePaddingHetero.p101 0
...
143 1 byte BytePaddingHetero.p162 0
144 1 byte BytePaddingHetero.p163 0
145 7 (loss due to the next object alignment)
Instance size: 152 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
10.3. @Contended
JDK 庫是通過引入私有的 @Contended 注解來消除這個性能敏感問題的抬吟。它在 JDK 中使用的也不多萨咕,例如在 java.lang.Thread
中保存線程本地的隨機數(shù)生成器狀態(tài):
public class Thread implements Runnable {
...
// The following three initially uninitialized fields are exclusively
// managed by class java.util.concurrent.ThreadLocalRandom. These
// fields are used to build the high-performance PRNGs in the
// concurrent code, and we can not risk accidental false sharing.
// Hence, the fields are isolated with @Contended.
/** The current seed for a ThreadLocalRandom */
@jdk.internal.vm.annotation.Contended("tlr")
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@jdk.internal.vm.annotation.Contended("tlr")
int threadLocalRandomProbe;
/** Secondary seed isolated from public ThreadLocalRandom sequence */
@jdk.internal.vm.annotation.Contended("tlr")
int threadLocalRandomSecondarySeed;
...
}
… 這使得字段布局器對它們特殊處理:
$ jdk8-64/bin/java -jar jol-cli.jar internals java.lang.Thread
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
java.lang.Thread object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 48 69 00 00
12 4 int Thread.priority 5
16 8 long Thread.eetop 0
...
96 4 j.l.Object Thread.blockerLock (object)
100 4 j.l.UEH Thread.uncaughtExceptionHandler null
104 128 (alignment/padding gap)
232 8 long Thread.threadLocalRandomSeed 0
240 4 int Thread.threadLocalRandomProbe 0
244 4 int Thread.threadLocalRandomSecondarySeed 0
248 128 (loss due to the next object alignment)
Instance size: 376 bytes
Space losses: 129 bytes internal + 128 bytes external = 257 bytes total
DDIQ: 為什么
@Contended
不是一個公開的注解?允許使用者構(gòu)建巨大的“普通”對象存在安全/可靠性上的影響火本。我們就談到這里吧危队。
通過其它實現(xiàn)細節(jié)中的捎帶,不依賴內(nèi)部注解也可以實現(xiàn)同樣的效果钙畔,我們接下來將會討論交掏。
11. 層次結(jié)構(gòu)中的字段布局
層次結(jié)構(gòu)中的字段布局需要特殊考慮。假如我們有這些類:
public class Hierarchy {
static class A {
int a;
}
static class B extends A {
int b;
}
static class C extends A {
int c;
}
}
這些類的層次結(jié)構(gòu)將會像這樣:
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . Hierarchy\$A
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
Hierarchy$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 28 b8 0f 00
12 4 int A.a 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
Hierarchy$B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 08 ba 0f 00
12 4 int A.a 0
16 4 int B.b 0
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
Hierarchy$C object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 08 ba 0f 00
12 4 int A.a 0
16 4 int C.c 0
20 4 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
注意:所有的類的 A.a
父類字段都在同樣的位置刃鳄。這使得可以從 A
的任意子類直接轉(zhuǎn)為 A
,然后在不檢查 A
的實際類型的情況下訪問 a
字段钱骂。也就是說叔锐,無論操作的是 A
、B
或 C
的實例见秽,對于 ((A)o).a
總是訪問同樣的偏移量愉烙。
這看起來像總是首先處理父類字段。這意味著父類字段總是位于層次結(jié)構(gòu)最前面么解取?這是一個實現(xiàn)細節(jié):在 JDK 15 之前步责,答案是“對的”;在 JDK15 之后,答案是“不對”蔓肯。我們將會通過一些觀察對它進行量化遂鹊。
11.1. 父類空白
在 JDK 15 之前,字段布局器僅僅對當前類局部聲明的字段生效蔗包。這意味著如果存在子類字段可以占用的父類空白秉扑,它們不會使用。讓我們將前面的 LongIntCarrier
分割為子類:
public class LongIntCarrierSubs {
static class A {
long value;
}
static class B extends A {
int somethingElse;
}
}
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . LongIntCarrierSubs\$B
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
Instantiated the sample instance via default constructor.
LongIntCarrierSubs$B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 08 ba 0f 00
12 4 (alignment/padding gap)
16 8 long A.value 0
24 4 int B.somethingElse 0
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 4 bytes internal + 4 bytes external = 8 bytes total
可以看到同樣由 long
對齊造成的空白调限。理論上來說舟陆,B.somethingElse
可以使用這部分空間,但是字段布局器的實現(xiàn)使得這不可能耻矮。因此秦躯,我們將 B
字段布局在 A
字段后,這浪費了 8 字節(jié)裆装。
11.2. 層次空白
JDK 15 之前的另外一個怪事是踱承,字段布局器以引用大小的整數(shù)單位對字段塊進行計數(shù),這使得子類字段塊從更遠的偏移開始米母。帶有很多小字段的場景比較明顯:
public class ThreeBooleanStooges {
static class A {
boolean a;
}
static class B extends A {
boolean b;
}
static class C extends B {
boolean c;
}
}
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . ThreeBooleanStooges\$A
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . ThreeBooleanStooges\$B
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . ThreeBooleanStooges\$C
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
ThreeBooleanStooges$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 28 b8 0f 00
12 1 boolean A.a false
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
ThreeBooleanStooges$B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 08 ba 0f 00
12 1 boolean A.a false
13 3 (alignment/padding gap)
16 1 boolean B.b false
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 3 bytes internal + 7 bytes external = 10 bytes total
ThreeBooleanStooges$C object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) e8 bb 0f 00
12 1 boolean A.a false
13 3 (alignment/padding gap)
16 1 boolean B.b false
17 3 (alignment/padding gap)
20 1 boolean C.c false
21 3 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 6 bytes internal + 3 bytes external = 9 bytes total
損失非常顯著勾扭!每個類實例浪費了 3 字節(jié),然后由于對象對齊有損失了一部分铁瞒。
在更大的堆或者沒有壓縮引用的情況下妙色,情況更嚴重。
$ jdk8-64/bin/java -Xmx64g -jar jol-cli.jar internals -cp . ThreeBooleanStooges\$C
# Running 64-bit HotSpot VM.
Instantiated the sample instance via default constructor.
ThreeBooleanStooges$C object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) b0 89 aa 37
12 4 (object header) b0 7f 00 00
16 1 boolean A.a false
17 7 (alignment/padding gap)
24 1 boolean B.b false
25 7 (alignment/padding gap)
32 1 boolean C.c false
33 7 (loss due to the next object alignment)
Instance size: 40 bytes
Space losses: 14 bytes internal + 7 bytes external = 21 bytes total
11.3. 觀察:層次結(jié)構(gòu)填充技巧
該實現(xiàn)的特殊性允許構(gòu)造一個相當奇怪的填充技巧慧耍,相對于 C 樣式的填充更具有彈性身辨。
public class HierarchyLongPadding {
static class Pad1 {
long l01, l02, l03, l04, l05, l06, l07, l08;
}
static class Carrier extends Pad1 {
byte pleaseHelpMe;
}
static class Pad2 extends Carrier {
long l11, l12, l13, l14, l15, l16, l17, l18;
}
static class UsableObject extends Pad2 {};
}
…?生成:
$ jdk8-64/bin/java -jar jol-cli.jar internals -cp . HierarchyLongPadding\$UsableObject
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
Instantiated the sample instance via default constructor.
HierarchyLongPadding$UsableObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) c8 bd 0f 00
12 4 (alignment/padding gap)
16 8 long Pad1.l01 0
24 8 long Pad1.l02 0
32 8 long Pad1.l03 0
40 8 long Pad1.l04 0
48 8 long Pad1.l05 0
56 8 long Pad1.l06 0
64 8 long Pad1.l07 0
72 8 long Pad1.l08 0
80 1 byte Carrier.pleaseHelpMe 0
81 7 (alignment/padding gap)
88 8 long Pad2.l11 0
96 8 long Pad2.l12 0
104 8 long Pad2.l13 0
112 8 long Pad2.l14 0
120 8 long Pad2.l15 0
128 8 long Pad2.l16 0
136 8 long Pad2.l17 0
144 8 long Pad2.l18 0
Instance size: 152 bytes
Space losses: 11 bytes internal + 0 bytes external = 11 bytes total
請看,我們利用了一個怪異的實現(xiàn)細節(jié)芍碧,將要保護的字段包圍在兩個類之間煌珊,
11.4. Java 15+ 中的層次空白
現(xiàn)在我們進入了 JDK 15,它的字段布局策略進行了全面改革泌豆。父類和層次結(jié)構(gòu)的空白已經(jīng)被消除定庵。執(zhí)行前面的例子將會顯示:
$ jdk15-64/bin/java -jar jol-cli.jar internals -cp . LongIntCarrierSubs\$B
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
Instantiated the sample instance via default constructor.
LongIntCarrierSubs$B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 4c 7d 17 00
12 4 int B.somethingElse 0
16 8 long A.value 0
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
最終,B.somethingElse
占用了父類 A.value
前面的對齊空白踪危。
層次結(jié)構(gòu)之間空白也沒有了:
$ jdk15-64/bin/java -jar jol-cli.jar internals -cp . ThreeBooleanStooges\$C
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
Instantiated the sample instance via default constructor.
ThreeBooleanStooges$C object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 90 7d 17 00
12 1 boolean A.a false
13 1 boolean B.b false
14 1 boolean C.c false
15 1 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 1 bytes external = 1 bytes total
完美蔬浙!
11.5. 觀察: 層次結(jié)構(gòu)填充技巧在 JDK 15 中失效了
很不幸,這使得幼稚的層次結(jié)構(gòu)填充技巧失效了贞远!請看:
$ jdk15-64/bin/java -jar jol-cli.jar internals -cp . HierarchyLongPadding\$UsableObject
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
Instantiated the sample instance via default constructor.
HierarchyLongPadding$UsableObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 08 7c 17 00
12 1 byte Carrier.pleaseHelpMe 0 # WHOOPS
13 3 (alignment/padding gap)
16 8 long Pad1.l01 0
24 8 long Pad1.l02 0
32 8 long Pad1.l03 0
40 8 long Pad1.l04 0
48 8 long Pad1.l05 0
56 8 long Pad1.l06 0
64 8 long Pad1.l07 0
72 8 long Pad1.l08 0
80 8 long Pad2.l11 0
88 8 long Pad2.l12 0
96 8 long Pad2.l13 0
104 8 long Pad2.l14 0
112 8 long Pad2.l15 0
120 8 long Pad2.l16 0
128 8 long Pad2.l17 0
136 8 long Pad2.l18 0
Instance size: 144 bytes
Space losses: 3 bytes internal + 0 bytes external = 3 bytes total
現(xiàn)在 pleaseHelpMe
占用了父類中的空白畴博,字段布局器將它提取出來了。哎呀蓝仲。
我感覺唯一的解決方法是填充最小的數(shù)據(jù)類型:
public class HierarchyBytePadding {
static class Pad1 {
byte p000, p001, p002, p003, p004, p005, p006, p007;
byte p008, p009, p010, p011, p012, p013, p014, p015;
byte p016, p017, p018, p019, p020, p021, p022, p023;
byte p024, p025, p026, p027, p028, p029, p030, p031;
byte p032, p033, p034, p035, p036, p037, p038, p039;
byte p040, p041, p042, p043, p044, p045, p046, p047;
byte p048, p049, p050, p051, p052, p053, p054, p055;
byte p056, p057, p058, p059, p060, p061, p062, p063;
}
static class Carrier extends Pad1 {
byte pleaseHelpMe;
}
static class Pad2 extends Carrier {
byte p100, p101, p102, p103, p104, p105, p106, p107;
byte p108, p109, p110, p111, p112, p113, p114, p115;
byte p116, p117, p118, p119, p120, p121, p122, p123;
byte p124, p125, p126, p127, p128, p129, p130, p131;
byte p132, p133, p134, p135, p136, p137, p138, p139;
byte p140, p141, p142, p143, p144, p145, p146, p147;
byte p148, p149, p150, p151, p152, p153, p154, p155;
byte p156, p157, p158, p159, p160, p161, p162, p163;
}
static class UsableObject extends Pad2 {};
}
…? 這填滿了所有空白俱病,不會使受保護的字段移動:
$ jdk15-64/bin/java -jar jol-cli.jar internals -cp . HierarchyBytePadding\$UsableObject
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
Instantiated the sample instance via default constructor.
HierarchyBytePadding$UsableObject object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00
4 4 (object header) 00 00 00 00
8 4 (object header) 08 7c 17 00
12 1 byte Pad1.p000 0
13 1 byte Pad1.p001 0
...
74 1 byte Pad1.p062 0
75 1 byte Pad1.p063 0
76 1 byte Carrier.pleaseHelpMe 0 # GOOD
77 1 byte Pad2.p100 0
78 1 byte Pad2.p101 0
...
139 1 byte Pad2.p162 0
140 1 byte Pad2.p163 0
141 3 (loss due to the next object alignment)
Instance size: 144 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
實際上官疲,這就是 JMH 現(xiàn)在的做法。
這仍然依賴于實現(xiàn)細節(jié)亮隙,Pad1
中的字段將會首先被處理途凫,并且填充父類中的空白。
12. 總結(jié)
Java 對象內(nèi)部很復雜咱揍,并且充滿靜態(tài)和動態(tài)的權(quán)衡折中颖榜。Java 對象大小可能根據(jù)內(nèi)部因素改變,例如 JVM 位數(shù)煤裙、JVM 特性集合等掩完。也可能根據(jù)運行時配置改變,例如堆內(nèi)存大小硼砰、壓縮引用模式且蓬、使用的 GC。
從 JVM 的角度看內(nèi)存占用题翰,可以看到壓縮引用扮演者重要的角色恶阴。即使不涉及引用,這也影響 class word 是否壓縮豹障。mark word 在 32 位 VM 中會更緊湊冯事,所以這也會改善內(nèi)存占用。(這還沒有提及 VM 本地指針和機器字長范圍的類型將會更醒)
從 Java(開發(fā)者)的角度來看昵仅,知道這些對象內(nèi)部結(jié)構(gòu)可以將字段隱藏在對象對齊陰影中,或者字段對齊空白中累魔,而且不會增加實例的大小摔笤。另一方面,僅僅增加一個小字段也可能導致實例大小顯著增加垦写,要解釋其中的原因?qū)豢杀苊獾纳婕皩毩6葘ο蠼Y(jié)構(gòu)的分析吕世。
最后,但并非最不重要的是梯投,通過一些技巧使得對象布局器按照某種順序放置是很困難的命辖,這依賴一些實現(xiàn)細節(jié)。但是這仍然可用分蓖,there are less safer and more safer things to rely on. 無論如何吮龄,需要對每次 JDK 升級進行額外的驗證。對于 JDK 15 和之后的版本咆疗,絕對應該重新驗證。
[1]. 實際上母债,在 Instrumentation.getObjectSize
中有一個午磁,它需要將其附加為 JavaAgent 執(zhí)行尝抖。
[2]. 你仍然可以使用,構(gòu)建一下迅皇,或者從某個地方獲取一個 fastdebug 構(gòu)建昧辽。例如這里。
[3]. 通過一些 VM 消費已經(jīng)使該方法可用登颓,但是最終由于對 Unsafe 的外部依賴太多搅荞,而導致不可用。
[4]. 這不是對象地址(熵很低)框咙,而是某個內(nèi)部 PRNG 的結(jié)果咕痛。
[5]. 在 Hotspot 中,-XX:+CompressedKlassPointers
基于 -XX:-CompressedOops
喇嘱,單這是實現(xiàn)上的限制茉贡,不是設計上的。理論上來說者铜,你可以在壓縮 oops 的情況下壓縮 klass 指針腔丧,但是這又會讓你維護一個配置。
Last updated 2020-04-21 10:50:39 CEST