64位java_99.9%的Java程序员都说不清的问题:JVM中的对象内存布局?
點(diǎn)擊上方石杉的架構(gòu)筆記,右上選擇“設(shè)為星標(biāo)”
每日早8點(diǎn)半,精品技術(shù)文章準(zhǔn)時(shí)送上
往期文章
BAT 面試官是如何360°無(wú)死角考察候選人的(上篇)
每秒上萬(wàn)并發(fā)下的Spring Cloud參數(shù)優(yōu)化實(shí)戰(zhàn)
分布式事務(wù)如何保障實(shí)際生產(chǎn)中99.99%高可用
記一位朋友斬獲 BAT 技術(shù)專家Offer的面試經(jīng)歷
億級(jí)流量架構(gòu)系列之如何支撐百億級(jí)數(shù)據(jù)的存儲(chǔ)與計(jì)算
作者:李瑞杰
目前就職于阿里巴巴,資深 JVM 研究人員
在 Java 程序中,我們擁有多種新建對(duì)象的方式。除了最為常見(jiàn)的 new 語(yǔ)句之外,我們還可以通過(guò)反射機(jī)制、Object.clone 方法、反序列化以及 Unsafe.allocateInstance 方法來(lái)新建對(duì)象。
其中,Object.clone 方法和反序列化通過(guò)直接復(fù)制已有的數(shù)據(jù),來(lái)初始化新建對(duì)象的實(shí)例字段。
Unsafe.allocateInstance 方法則沒(méi)有初始化實(shí)例字段,而 new 語(yǔ)句和反射機(jī)制,則是通過(guò)調(diào)用構(gòu)造器來(lái)初始化實(shí)例字段。????? ?
?我們先來(lái)考察new語(yǔ)句,準(zhǔn)備一個(gè)類,如下圖所示
????????????? ??
讓我們編譯他的字節(jié)碼:
????? ??
可以看到,new語(yǔ)句編譯而成的字節(jié)碼將包含用來(lái)請(qǐng)求內(nèi)存的 new 指令,以及用來(lái)調(diào)用構(gòu)造器的 invokespecial 指令。????? ??
本文不是專門介紹invoke系列指令的,我會(huì)在后面的文章中介紹invoke系列指令。
不過(guò)在這里我多說(shuō)一嘴,字節(jié)碼中的invokespecial指令通常用于調(diào)用私有實(shí)例方法、構(gòu)造器,以及使用super關(guān)鍵字調(diào)用父類的實(shí)例方法或構(gòu)造器,和所實(shí)現(xiàn)接口的默認(rèn)方法。
提到構(gòu)造器,就不得不提到 Java 對(duì)構(gòu)造器的諸多約束。首先,如果一個(gè)類沒(méi)有定義任何構(gòu)造器的話, Java 編譯器會(huì)自動(dòng)添加一個(gè)無(wú)參數(shù)的構(gòu)造器。????? ??
我們剛才的TestNew類,他的字節(jié)碼編譯出來(lái)后,有下面的片段。
????? ??
在JAVA源碼中,我們沒(méi)有定義構(gòu)造器,但是生成出來(lái)的字節(jié)碼,已經(jīng)自動(dòng)幫我們添加了一個(gè)無(wú)參數(shù)的構(gòu)造器。他使用的invokespecial方法最終調(diào)用的是其父類Object類的構(gòu)造器方法。? ? ? ??
我將講述JVM的構(gòu)造器調(diào)用原則,那就是,如果子類的構(gòu)造器需要調(diào)用父類的構(gòu)造器。如果父類存在無(wú)參數(shù)構(gòu)造器的話,該調(diào)用可以是隱式的。也就是說(shuō), Java 編譯器會(huì)自動(dòng)添加對(duì)父類構(gòu)造器的調(diào)用。
但是,如果父類沒(méi)有無(wú)參數(shù)構(gòu)造器,那么子類的構(gòu)造器則需要顯式地調(diào)用父類帶參數(shù)的構(gòu)造器。
顯式調(diào)用有兩種,一是直接使用“super”關(guān)鍵字調(diào)用父類構(gòu)造器,二是使用“this”關(guān)鍵字調(diào)用同一個(gè)類中的其他構(gòu)造器。
無(wú)論是直接的顯式調(diào)用,還是間接的顯式調(diào)用,都需要作為構(gòu)造器的第一條語(yǔ)句,以便優(yōu)先初始化繼承而來(lái)的父類字段。
可以不優(yōu)先初始化繼承來(lái)的父類字段嗎?可以,如果你能使用字節(jié)碼注入工具的話。
當(dāng)我們調(diào)用一個(gè)構(gòu)造器時(shí),它將優(yōu)先調(diào)用父類的構(gòu)造器,直至 Object 類。這些構(gòu)造器的調(diào)用者皆為同一對(duì)象,也就是通過(guò) new 指令新建而來(lái)的對(duì)象。? ? ? ??
事實(shí)上,我上面的陳述意味著:通過(guò) new 指令新建出來(lái)的對(duì)象,它的內(nèi)存其實(shí)涵蓋了所有父類中的實(shí)例字段。
也就是說(shuō),雖然子類無(wú)法訪問(wèn)父類的私有實(shí)例字段,或者子類的實(shí)例字段隱藏了父類的同名實(shí)例字段,但是子類的實(shí)例還是會(huì)為這些父類實(shí)例字段分配內(nèi)存的。????? ??
下面我將介紹壓縮指針技術(shù)。在 Java 虛擬機(jī)中,每個(gè) Java 對(duì)象都有一個(gè)對(duì)象頭,它由標(biāo)記字段和類型指針?biāo)鶚?gòu)成。
標(biāo)記字段用以存儲(chǔ) Java 虛擬機(jī)有關(guān)該對(duì)象的運(yùn)行數(shù)據(jù),如哈希碼、GC 信息以及鎖信息,而類型指針則指向該對(duì)象的類。????????
在64位的JVM中,對(duì)象頭的標(biāo)記字段占 64 位,而類型指針又占了 64 位。也就是說(shuō),每一個(gè) Java 對(duì)象在內(nèi)存中的額外開(kāi)銷就是 16 個(gè)字節(jié)。????????
為了盡量較少對(duì)象的內(nèi)存使用量,64位JVM引入了壓縮指針的概念,將堆中原本64位的Java對(duì)象指針壓縮成32位的。????????
這樣一來(lái),對(duì)象頭中的類型指針也會(huì)被壓縮成32位,使得對(duì)象頭的大小從16字節(jié)降至12字節(jié)。
當(dāng)然,壓縮指針不僅可以作用于對(duì)象頭的類型指針,還可以作用于引用類型的字段,以及引用類型數(shù)組。????? ??
它的原理是什么?答案是內(nèi)存對(duì)齊。
我們規(guī)定,默認(rèn)情況下,JVM堆中對(duì)象的起始地址需要對(duì)齊至8的倍數(shù),如果一個(gè)對(duì)象用不到8N 個(gè)字節(jié),那么空白的那部分空間就浪費(fèi)掉了,這些浪費(fèi)掉的空間我們稱之為對(duì)象間的填充。
大家知道,指針里面存放的是地址,由于堆中對(duì)象的起始地址是對(duì)齊至8的倍數(shù),所以指針存放一個(gè)引用(或者對(duì)象的類)的內(nèi)存地址時(shí),根本就不用存放最后的三位二進(jìn)制數(shù)。
因?yàn)樗袑?duì)象或類的內(nèi)存地址都對(duì)齊了8,所以他們的內(nèi)存地址的最低三位總是0,32位的指針就可以尋址到 2 的 35 次方個(gè)字節(jié),也就是 32GB 的地址空間(超過(guò) 32GB 則會(huì)關(guān)閉壓縮指針)。
我們可以通過(guò)配置虛擬機(jī)的內(nèi)存對(duì)齊選項(xiàng)來(lái)進(jìn)一步提升尋址范圍。但是,這同時(shí)也可能增加對(duì)象間填充,導(dǎo)致壓縮指針沒(méi)有達(dá)到原本節(jié)省空間的效果。????????
就算是關(guān)閉了壓縮指針,Java 虛擬機(jī)還是會(huì)進(jìn)行內(nèi)存對(duì)齊。此外,內(nèi)存對(duì)齊不僅存在于對(duì)象與對(duì)象之間,也存在于對(duì)象中的字段之間。
比如說(shuō),Java 虛擬機(jī)要求long字段、double字段,以及非壓縮指針狀態(tài)下的引用字段地址為8的倍數(shù)。????? ??
這是為什么呢?
CPU的緩存行機(jī)制大家應(yīng)該有所耳聞,如果字段不是對(duì)齊的,那么就有可能出現(xiàn)跨緩存行的字段。
該字段的讀取可能需要替換兩個(gè)緩存行,而該字段的存儲(chǔ)也會(huì)同時(shí)污染兩個(gè)緩存行。
我們將在后期文章關(guān)于volatile關(guān)鍵詞的本質(zhì)分析的過(guò)程中,再次考察到CPU緩存行的相關(guān)機(jī)制。????? ??
最后我要提一句的是,字段重排列技術(shù),就是我剛才提到的,對(duì)象的字段之間存在的內(nèi)存對(duì)齊。這指的是重新分配字段的先后順序,以達(dá)到內(nèi)存對(duì)齊的目的
它有以下兩個(gè)規(guī)則:? ? ??
其一,如果一個(gè)字段占據(jù)C個(gè)字節(jié),那么該字段的偏移量需要對(duì)齊至NC。這里的偏移量指的是字段地址與對(duì)象的起始地址差值。????????
以Long類為例,它僅有一個(gè)long類型的實(shí)例字段。在使用了壓縮指針的 64 位虛擬機(jī)中,盡管對(duì)象頭的大小為12個(gè)字節(jié),該 long 類型字段的偏移量也只能是16,而中間空著的4個(gè)字節(jié)便會(huì)被浪費(fèi)掉。????????
其二,子類所繼承字段的偏移量,需要與父類對(duì)應(yīng)字段的偏移量保持一致。
說(shuō)白了,比如B繼承了A,A是B的父類,A中所有的字段,在B中都有,而且是先放A的字段,再放B的字段。而且B類對(duì)象放A類字段時(shí),需要與父類對(duì)應(yīng)字段的偏移量保持一致。
接下來(lái)我說(shuō)一個(gè)拓展內(nèi)容吧,什么是虛共享?
假設(shè)兩個(gè)線程分別訪問(wèn)同一對(duì)象中不同的 volatile 字段,邏輯上它們并沒(méi)有共享內(nèi)容,因此不需要同步。
如果這兩個(gè)字段恰好在同一個(gè)緩存行中,那么對(duì)這些字段的寫操作會(huì)導(dǎo)致緩存行的寫回,也就造成了實(shí)質(zhì)上的共享。
Java8還引入了一個(gè)新的注釋@Contended,用來(lái)解決對(duì)象字段之間的虛共享。
Java 虛擬機(jī)會(huì)讓不同的@Contended字段處于獨(dú)立的緩存行中,因此你會(huì)看到大量的空間被浪費(fèi)掉,避免無(wú)謂的緩存行同步操作。
具體的算法屬于實(shí)現(xiàn)細(xì)節(jié)了,大家有興趣可以去用:
-XX:-RestrictContended
這個(gè)虛擬機(jī)選項(xiàng),查看Contended字段的內(nèi)存布局。
END
劃至底部,點(diǎn)擊“在看”,是你來(lái)過(guò)的儀式感!
推薦閱讀:
簡(jiǎn)歷寫了會(huì)Kafka,面試官90%會(huì)讓你講講acks參數(shù)對(duì)消息持久化的影響!
面試最讓你手足無(wú)措的一個(gè)問(wèn)題:你的系統(tǒng)如何支撐高并發(fā)?
Java高階必備:如何優(yōu)化Spring Cloud微服務(wù)注冊(cè)中心架構(gòu)?
高并發(fā)場(chǎng)景下,如何保證生產(chǎn)者投遞到消息中間件的消息不丟失?
從團(tuán)隊(duì)自研的百萬(wàn)并發(fā)中間件系統(tǒng)的內(nèi)核設(shè)計(jì)看Java并發(fā)性能優(yōu)化!
如果20萬(wàn)用戶同時(shí)訪問(wèn)一個(gè)熱點(diǎn)緩存,如何優(yōu)化你的緩沖架構(gòu)?
更多文章:
2018年原創(chuàng)匯總
2019年原創(chuàng)匯總(持續(xù)更新)
爆款推薦
面試專欄
歡迎長(zhǎng)按下圖關(guān)注公眾號(hào)石杉的架構(gòu)筆記
BAT架構(gòu)經(jīng)驗(yàn)傾囊相授
總結(jié)
以上是生活随笔為你收集整理的64位java_99.9%的Java程序员都说不清的问题:JVM中的对象内存布局?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: flutter 图片转base64_京东
- 下一篇: 小米10谷歌连携失败_Android 1