大將生來膽氣豪 腰橫秋水雁翎刀
風(fēng)吹鼉鼓山河動 電閃旌旗日月高
天上麒麟原有種 穴中螻蟻豈能逃
太平待詔歸來日 朕與將軍解戰(zhàn)袍
開篇聊閑天锰瘸。
在使用Java的過程中,一直困擾我的一個問題是,一個對象到底占用多大內(nèi)存?(Java并沒有sizeof操作符) 但這個問題戚哎,結(jié)果卻并沒有那么簡單。
Java沒有sizeof也不需要sizeof操作符掀抹,所有數(shù)據(jù)類型的大小都在Java語言規(guī)范中定義,和機(jī)器平臺不相關(guān)心俗。但從Java虛擬機(jī)的角度傲武,一個Java中定義的對象,在虛擬機(jī)中占用多大內(nèi)存?這個問題好像只能通過分析虛擬機(jī)的實(shí)現(xiàn)來找到答案谱轨。這就牽扯到另一個問題,我們到底需不需要了解我們所使用工具的實(shí)現(xiàn)吠谢?
探索知識的任何階段都是存有疑惑的土童,就像中學(xué)和大學(xué)都有數(shù)學(xué),但學(xué)習(xí)深入的程度不同工坊,分別有不同方面的疑惑献汗。我們只是基于一些公知的認(rèn)識,使其作為本階段學(xué)習(xí)的起點(diǎn)王污,并以此展開上層的研究罢吃。
少部分人會有一個無止盡思考的奇怪思維現(xiàn)象。舉例來說昭齐,我們都知道地球圍繞太陽做周期性公轉(zhuǎn)尿招,又知道電子圍繞原子核做周期性公轉(zhuǎn)運(yùn)動,這和地球繞太陽公轉(zhuǎn)的行為如出一轍阱驾,不禁會讓人想就谜,太陽是不是相當(dāng)于原子核,地球相當(dāng)于一個電子里覆,我們都生活在一個電子上丧荐。而我們的是身體里有那么多原子和電子,我們的身體是否又定義了另一個新的宇宙喧枷。無盡的遐想虹统,無盡的疑惑,雖然有些荒誕隧甚,但并非完全不合理车荔。但是如果無休止地問下去,雖然會對底層的科學(xué)更加清晰戚扳,但是對上層的知識結(jié)構(gòu)的構(gòu)建非常不利夸赫,從而我們需要一個公設(shè),例如認(rèn)為原子是不可再分的咖城,沒有更小的對象了茬腿,一切理論研究以此為基礎(chǔ)展開。例如乘法是基于加法的宜雀,在計(jì)算3*4的結(jié)果時(shí)切平,必須不去質(zhì)疑為什么1+1=2這件事,并認(rèn)為它是真理辐董。在學(xué)習(xí)操作系統(tǒng)時(shí)悴品,不去思考硬件內(nèi)部究竟是如何工作的,只假設(shè)硬件是一個給定輸入有給定輸出的系統(tǒng)。
這里的Java虛擬機(jī)苔严,作為一個運(yùn)行字節(jié)碼的平臺定枷,顯然不能當(dāng)做一個公設(shè)來看待。虛擬機(jī)包含三個概念届氢,語言規(guī)范欠窒,具體實(shí)現(xiàn)和運(yùn)行實(shí)例。語言規(guī)范描述了虛擬機(jī)需要實(shí)現(xiàn)什么退子,而并沒有規(guī)定需要如何去實(shí)現(xiàn)岖妄。復(fù)雜的虛擬機(jī)(Hotspot/JRockit/J9)和簡單的虛擬機(jī)(Kaffe/Jamvm/cacaovm)在實(shí)現(xiàn)方面有很大的差異。對一個有不確定實(shí)現(xiàn)的工具寂祥,沖一杯咖啡荐虐,打開音樂,從源代碼的角度分析工具的實(shí)現(xiàn)丸凭,對上層知識的構(gòu)建非常有利(事實(shí)也證明福扬,Java使用者對虛擬機(jī)的了解程度,遠(yuǎn)遠(yuǎn)不如c/c++使用者對機(jī)器平臺的了解程度深)惜犀。
以Hotspot為例忧换,在虛擬機(jī)的內(nèi)部,通過instanceOopDesc來表示一個對象(OOP-Klass二分模型在另一個篇中寫)向拆,每個對象包含Mark Word和元數(shù)據(jù)指針作為對象頭亚茬,接下來依次是實(shí)例數(shù)據(jù)和padding:
Mark Word: 定義在oopDesc中的_mark成員,儲存對象運(yùn)行時(shí)的記錄信息浓恳,如HashCode/GC分代年齡狀態(tài)鎖標(biāo)志/線程持有的鎖/偏向線程ID/偏向時(shí)間戳等刹缝。_mark成員的數(shù)據(jù)類型為markOop,占用內(nèi)存大小與虛擬機(jī)word長度一致颈将。在32位虛擬機(jī)上為32位梢夯,在64位虛擬機(jī)上為64位(可以壓縮)。
元數(shù)據(jù)指針: 定義在oopDesc中的_metadata成員晴圾,指向描述類型的Klass對象指針颂砸。根據(jù)是否壓縮定義為一個union。虛擬機(jī)在運(yùn)行時(shí)頻繁使用這個指針定位到位于方法區(qū)的信息死姚。
虛擬機(jī)運(yùn)行時(shí)人乓,每創(chuàng)建一個對象,在虛擬機(jī)內(nèi)部就要創(chuàng)建相應(yīng)有對象頭的對象都毒,因此對象頭的布局對對象內(nèi)存空間利用率(Instance Data/Header+instanceData+padding)十分重要色罚。但是,在對象生命周期內(nèi)账劲,虛擬機(jī)要記錄很多信息戳护,如hashCode/GC分代年齡/鎖記錄指針/線程ID 等金抡,因此header必須要仔細(xì)設(shè)計(jì)。
設(shè)計(jì)1: 虛擬機(jī)配置選項(xiàng)-XX:UserCompressedOops腌且。其作用是在64位機(jī)器上梗肝,對_metadata成員使用32位指針存儲。在64位系統(tǒng)上铺董,指針類型為64位巫击,這樣一來,從32位系統(tǒng)遷移到64位系統(tǒng)時(shí)柄粹,內(nèi)存利用率就會有所下降喘鸟。union聯(lián)合體中wideKlassOop是指向klassOopDesc指針匆绣,而narrowOop是32位無符號整形驻右。
設(shè)計(jì)2: _mark成員模仿網(wǎng)絡(luò)協(xié)議報(bào)文頭部,把mark word劃分為多個比特區(qū)間崎淳,并在不同對象狀態(tài)下賦予每個bit不同含義堪夭。
Hotspot有三種對象成員排列順序: [oops,longs/doubles/ints/shorts/chars/bytes],[longs/doubles, ints, shorts/chars, bytes, oops] 和 [Fields allocation: oops fields in super and sub classes are together]拣凹。默認(rèn)為第二種順序森爽。源碼如下:
排列規(guī)則如下,每個對象內(nèi)存地址要是8的倍數(shù)嚣镜,每個成員的內(nèi)存地址要是自己大小的倍數(shù)爬迟,例如整形要是4字節(jié)的倍數(shù),long要是8字節(jié)的倍數(shù)菊匿。
假設(shè)付呕,在Java代碼中,定義如下類:
如果虛擬機(jī)不改變成員變量排列順序跌捆,32位機(jī)器徽职,在內(nèi)存中順序如下:
這樣有14字節(jié)因?yàn)閜adding被浪費(fèi)了。如果重新調(diào)整排序規(guī)則:
這樣只有6字節(jié)因?yàn)閜adding被浪費(fèi)佩厚。在每個成員都要內(nèi)存對齊的情況下姆钉,先分配大內(nèi)存的成員會節(jié)約內(nèi)存。
按照這個規(guī)則來計(jì)算Object對象的直接子類Boolean抄瓦,header+value+padding=8+1+7=16潮瓶,竟然需要16字節(jié)。
如果類不是直接繼承Object對象钙姊,即父類中如果有成員變量的話筋讨。舉例如下:
一個B的實(shí)例在內(nèi)存中看起來長這樣:
其他情況的排序不再贅述摸恍,可根據(jù)代碼自行排列悉罕。
了解Boolean類內(nèi)存利用率很低以后赤屋,再說一下HashMap。對于應(yīng)用層的程序來說壁袄,這簡直是神器类早,只要創(chuàng)建了之后就可以不斷的丟東西進(jìn)去,添加刪除都是O(1)操作嗜逻,又快又好涩僻。不過引用第一位圖靈獎獲得者Alan Perlis的名言:“Lisp programmers know the value of everything but the cost of nothing.”,目的是想提醒我們做事情不要忘記背后的代價(jià)栈顷。對于HashMap來說逆日,代價(jià)主要是內(nèi)存的開銷,試想一下萄凤,Java沒有HashMap<boolean,boolean>只有HashMap<Boolean,Boolean>室抽。