大將生來膽氣豪 腰橫秋水雁翎刀
風吹鼉鼓山河動 電閃旌旗日月高
天上麒麟原有種 穴中螻蟻豈能逃
太平待詔歸來日 朕與將軍解戰袍
開篇聊閑天。
在使用Java的過程中,一直困擾我的一個問題是,一個對象到底占用多大內存?(Java并沒有sizeof操作符) 但這個問題,結果卻并沒有那么簡單。
Java沒有sizeof也不需要sizeof操作符,所有數據類型的大小都在Java語言規范中定義,和機器平臺不相關。但從Java虛擬機的角度,一個Java中定義的對象,在虛擬機中占用多大內存?這個問題好像只能通過分析虛擬機的實現來找到答案。這就牽扯到另一個問題,我們到底需不需要了解我們所使用工具的實現?
探索知識的任何階段都是存有疑惑的,就像中學和大學都有數學,但學習深入的程度不同,分別有不同方面的疑惑。我們只是基于一些公知的認識,使其作為本階段學習的起點,并以此展開上層的研究。
少部分人會有一個無止盡思考的奇怪思維現象。舉例來說,我們都知道地球圍繞太陽做周期性公轉,又知道電子圍繞原子核做周期性公轉運動,這和地球繞太陽公轉的行為如出一轍,不禁會讓人想,太陽是不是相當于原子核,地球相當于一個電子,我們都生活在一個電子上。而我們的是身體里有那么多原子和電子,我們的身體是否又定義了另一個新的宇宙。無盡的遐想,無盡的疑惑,雖然有些荒誕,但并非完全不合理。但是如果無休止地問下去,雖然會對底層的科學更加清晰,但是對上層的知識結構的構建非常不利,從而我們需要一個公設,例如認為原子是不可再分的,沒有更小的對象了,一切理論研究以此為基礎展開。例如乘法是基于加法的,在計算3*4的結果時,必須不去質疑為什么1+1=2這件事,并認為它是真理。在學習操作系統時,不去思考硬件內部究竟是如何工作的,只假設硬件是一個給定輸入有給定輸出的系統。
這里的Java虛擬機,作為一個運行字節碼的平臺,顯然不能當做一個公設來看待。虛擬機包含三個概念,語言規范,具體實現和運行實例。語言規范描述了虛擬機需要實現什么,而并沒有規定需要如何去實現。復雜的虛擬機(Hotspot/JRockit/J9)和簡單的虛擬機(Kaffe/Jamvm/cacaovm)在實現方面有很大的差異。對一個有不確定實現的工具,沖一杯咖啡,打開音樂,從源代碼的角度分析工具的實現,對上層知識的構建非常有利(事實也證明,Java使用者對虛擬機的了解程度,遠遠不如c/c++使用者對機器平臺的了解程度深)。
以Hotspot為例,在虛擬機的內部,通過instanceOopDesc來表示一個對象(OOP-Klass二分模型在另一個篇中寫),每個對象包含Mark Word和元數據指針作為對象頭,接下來依次是實例數據和padding:
Mark Word: 定義在oopDesc中的_mark成員,儲存對象運行時的記錄信息,如HashCode/GC分代年齡狀態鎖標志/線程持有的鎖/偏向線程ID/偏向時間戳等。_mark成員的數據類型為markOop,占用內存大小與虛擬機word長度一致。在32位虛擬機上為32位,在64位虛擬機上為64位(可以壓縮)。
元數據指針: 定義在oopDesc中的_metadata成員,指向描述類型的Klass對象指針。根據是否壓縮定義為一個union。虛擬機在運行時頻繁使用這個指針定位到位于方法區的信息。
虛擬機運行時,每創建一個對象,在虛擬機內部就要創建相應有對象頭的對象,因此對象頭的布局對對象內存空間利用率(Instance Data/Header+instanceData+padding)十分重要。但是,在對象生命周期內,虛擬機要記錄很多信息,如hashCode/GC分代年齡/鎖記錄指針/線程ID 等,因此header必須要仔細設計。
設計1: 虛擬機配置選項-XX:UserCompressedOops。其作用是在64位機器上,對_metadata成員使用32位指針存儲。在64位系統上,指針類型為64位,這樣一來,從32位系統遷移到64位系統時,內存利用率就會有所下降。union聯合體中wideKlassOop是指向klassOopDesc指針,而narrowOop是32位無符號整形。
設計2: _mark成員模仿網絡協議報文頭部,把mark word劃分為多個比特區間,并在不同對象狀態下賦予每個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]。默認為第二種順序。源碼如下:
排列規則如下,每個對象內存地址要是8的倍數,每個成員的內存地址要是自己大小的倍數,例如整形要是4字節的倍數,long要是8字節的倍數。
假設,在Java代碼中,定義如下類:
如果虛擬機不改變成員變量排列順序,32位機器,在內存中順序如下:
這樣有14字節因為padding被浪費了。如果重新調整排序規則:
這樣只有6字節因為padding被浪費。在每個成員都要內存對齊的情況下,先分配大內存的成員會節約內存。
按照這個規則來計算Object對象的直接子類Boolean,header+value+padding=8+1+7=16,竟然需要16字節。
如果類不是直接繼承Object對象,即父類中如果有成員變量的話。舉例如下:
一個B的實例在內存中看起來長這樣:
其他情況的排序不再贅述,可根據代碼自行排列。
了解Boolean類內存利用率很低以后,再說一下HashMap。對于應用層的程序來說,這簡直是神器,只要創建了之后就可以不斷的丟東西進去,添加刪除都是O(1)操作,又快又好。不過引用第一位圖靈獎獲得者Alan Perlis的名言:“Lisp programmers know the value of everything but the cost of nothing.”,目的是想提醒我們做事情不要忘記背后的代價。對于HashMap來說,代價主要是內存的開銷,試想一下,Java沒有HashMap<boolean,boolean>只有HashMap<Boolean,Boolean>。