一個對象占多少字節?
關於對象的大小,對於C/C++來說,都是有sizeof函數可以直接獲取的,但是Java似乎沒有這樣的方法。不過還好,在JDK1.5之后引入了Instrumentation類,這個類提供了計算對象內存占用量的方法。至於具體Instrumentation類怎么用就不說了,可以參看這篇文章如何精確地測量java對象的大小。
不過有一點不同的是,這篇文章使用命令行傳入JVM參數來指定代理,這里我通過Eclipse設置JVM參數:
后面的是我打的agent.jar的具體路徑。剩下的就不說了,看一下測試代碼:
1 public class JVMSizeofTest { 2 3 @Test 4 public void testSize() { 5 System.out.println("Object對象的大小:" + JVMSizeof.sizeOf(new Object()) + "字節"); 6 System.out.println("字符a的大小:" + JVMSizeof.sizeOf('a') + "字節"); 7 System.out.println("整型1的大小:" + JVMSizeof.sizeOf(new Integer(1)) + "字節"); 8 System.out.println("字符串aaaaa的大小:" + JVMSizeof.sizeOf(new String("aaaaa")) + "字節"); 9 System.out.println("char型數組(長度為1)的大小:" + JVMSizeof.sizeOf(new char[1]) + "字節"); 10 } 11 12 }
運行結果為:
Object對象的大小:16字節
字符a的大小:16字節
整型1的大小:16字節
字符串aaaaa的大小:24字節
char型數組(長度為1)的大小:24字節
接着,代碼不變,加入一條虛擬機參數"-XX:-UseCompressedOops",再運行一遍測試類,運行結果為:
Object對象的大小:16字節
字符a的大小:24字節
整型1的大小:24字節
字符串aaaaa的大小:32字節
char型數組(長度為1)的大小:32字節
后文來詳細解釋一下原因。
Java對象大小計算方法
JVM對於普通對象和數組對象的大小計算方式有所不同,我畫了一張圖說明:
解釋一下其中每個部分:
- Mark Word:存儲對象運行時記錄信息,占用內存大小與機器位數一樣,即32位機占4字節,64位機占8字節
- 元數據指針:指向描述類型的Klass對象(Java類的C++對等體)的指針,Klass對象包含了實例對象所屬類型的元數據,因此該字段被稱為元數據指針,JVM在運行時將頻繁使用這個指針定位到位於方法區內的類型信息。這個數據的大小稍后說
- 數組長度:數組對象特有,一個指向int型的引用類型,用於描述數組長度,這個數據的大小和元數據指針大小相同,同樣稍后說
- 實例數據:實例數據就是8大基本數據類型byte、short、int、long、float、double、char、boolean(對象類型也是由這8大基本數據類型復合而成),每種數據類型占多少字節就不一一例舉了
- 填充:不定,HotSpot的對齊方式為8字節對齊,即一個對象必須為8字節的整數倍,因此如果最后前面的數據大小為17則填充7,前面的數據大小為18則填充6,以此類推
為了保證效率,Java編譯期在編譯Java對象的時候,通過字段類型對Java對象的字段會進行排序,具體順序如下表所示:
了解這個是很有用的,我們可以通過在字段時間通過填充長整型變量的方式把熱點變量隔離在不同的緩存行中,減少偽同步,在多核CPU中極大地提升效率,這個以后有機會寫文章專門講解。
最后再說說元數據指針的大小。元數據指針是一個引用類型,因此正常來說64位機元數據指針應當為8字節,32位機元數據指針應當為4字節,但是HotSpot中有一項優化是對元數據類型指針進行壓縮存儲,使用JVM參數:
- -XX:+UseCompressedOops開啟壓縮
- -XX:-UseCompressedOops關閉壓縮
HotSpot默認是前者,即開啟元數據指針壓縮,當開啟壓縮的時候,64位機上的元數據指針將占據4個字節的大小。換句話說就是當開啟壓縮的時候,64位機上的引用將占據4個字節,否則是正常的8字節。
Java對象內存大小計算
有了上面的理論基礎,我們就可以分析JVMSizeofTest類的執行結果及為什么加入了"-XX:-UseCompressedOops"這條參數后同一個對象的大小會有差異了。
首先是Object對象的大小:
- 開啟指針壓縮時,8字節Mark Word + 4字節元數據指針 = 12字節,由於12字節不是8的倍數,因此填充4字節,對象Object占據16字節內存
- 關閉指針壓縮時,8字節Mark Word + 8字節元數據指針 = 16字節,由於16字節正好是8的倍數,因此不需要填充字節,對象Object占據16字節內存
接着是字符'a'的大小:
- 開啟指針壓縮時,8字節Mark Word + 4字節元數據指針 + 1字節char = 13字節,由於13字節不是8的倍數,因此填充3字節,字符'a'占據16字節內存
- 關閉指針壓縮時,8字節Mark Word + 8字節元數據指針 + 1字節char = 17字節,由於17字節不是8的倍數,因此填充7字節,字符'a'占據24字節內存
接着是整型1的大小:
- 開啟指針壓縮時,8字節Mark Word + 4字節元數據指針 + 4字節int = 16字節,由於16字節正好是8的倍數,因此不需要填充字節,整型1占據16字節內存
- 關閉指針壓縮時,8字節Mark Word + 8字節元數據指針 + 4字節int = 20字節,由於20字節正好是8的倍數,因此填充4字節,整型1占據24字節內存
接着是字符串"aaaaa"的大小,所有靜態字段不需要管,只關注實例字段,String對象中實例字段有"char value[]"與"int hash",由此可知:
- 開啟指針壓縮時,8字節Mark Word + 4字節元數據指針 + 4字節引用 + 4字節int = 20字節,由於20字節不是8的倍數,因此填充4字節,字符串"aaaaa"占據24字節內存
- 關閉指針壓縮時,8字節Mark Word + 8字節元數據指針 + 8字節引用 + 4字節int = 28字節,由於28字節不是8的倍數,因此填充4字節,字符串"aaaaa"占據32字節內存
最后是長度為1的char型數組的大小:
- 開啟指針壓縮時,8字節的Mark Word + 4字節的元數據指針 + 4字節的數組大小引用 + 1字節char = 17字節,由於17字節不是8的倍數,因此填充7字節,長度為1的char型數組占據24字節內存
- 關閉指針壓縮時,8字節的Mark Word + 8字節的元數據指針 + 8字節的數組大小引用 + 1字節char = 25字節,由於25字節不是8的倍數,因此填充7字節,長度為1的char型數組占據32字節內存
Mark Word
Mark Word前面已經看到過了,它是Java對象頭中很重要的一部分。Mark Word存儲的是對象自身的運行數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標識、線程持有的鎖、偏向線程ID、偏向時間戳等等。
不過由於對象需要存儲的運行時數據很多,其實已經超出了32位、64位Bitmap結構所能記錄的限度,但是對象頭是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲盡量多的信息。例如在32位的HotSpot虛擬機中對象未被鎖定的狀態下,Mark Word的32個Bits空間中的25Bits用於存儲對象哈希碼(HashCode),4Bits用於存儲對象分代年齡,2Bits用於存儲鎖標識位,1Bit固定位0。在其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容如下圖所示:
這里要特別關注的是鎖狀態,后文將對鎖狀態及鎖狀態的變化進行研究。
鎖的升級
如上圖所示,鎖的狀態共有四種:無鎖態、偏向鎖、輕量級鎖和重量級鎖,其中偏向鎖和輕量級鎖是JDK1.6開始為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的。
四種鎖的狀態會隨着競爭情況逐漸升級,鎖可以升級但是不能降級,意味着偏向鎖可以升級為輕量級鎖但是輕量級鎖不能降級為偏向鎖,目的是為了提高獲得鎖和釋放鎖的效率。用一張圖表示這種關系:
偏向鎖
HotSpot作者經過以往的研究發現大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代碼更低因此引入了偏向鎖。偏向鎖的獲取過程為:
- 訪問Mark Word中偏向鎖的標識是否設置為1,所標志位是否為01----確認為可偏向狀態
- 如果為可偏向狀態,則測試線程id是否指向當前線程,如果是,執行(5),否則執行(3)
- 如果線程id並為指向當前線程,通過CAS操作競爭鎖。如果競爭成功,則將Mark Word中的線程id設置為當前線程id,然后執行(5);如果競爭失敗,執行(4)
- 如果CAS獲取偏向鎖失敗,則表示有競爭。當達到全局安全點(safepoint)時獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖(因為偏向鎖是假設沒有競爭,但是這里出現了競爭,要對偏向鎖進行升級),然后被阻塞在安全點的線程繼續往下執行同步代碼
- 執行同步代碼
有獲取就有釋放,偏向鎖的釋放點在於上述的第(4)步,只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的釋放過程為:
- 需要等待全局安全點(在這個時間點上沒有字節碼正在執行)
- 它會首先暫停擁有偏向鎖的線程,判斷鎖對象是否處於被鎖定狀態
- 偏向鎖釋放后恢復到未鎖定(標識位為01)或輕量級鎖(標識位為00)狀態
輕量級鎖
輕量級鎖的加鎖過程為:
- 在代碼進入同步塊的時候,如果同步對象鎖狀態為無鎖狀態,JVM首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之為Displaced Mark Word,此時線程堆棧與對象頭的狀態如圖所示
- 拷貝對象頭中的Mark Word復制到鎖記錄中
- 拷貝成功后,JVM將使用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,並將Lock Record里的owner指針指向Object Mark Word,如果更新成功,則執行步驟(4),否則執行步驟(5)
- 如果更新動作成功,那么當前線程就擁有了該對象的鎖,並且對象Mark Word的鎖標識位設置為00,即表示此對象處於輕量級鎖狀態,此時線堆棧與對象頭的狀態如圖所示
- 如果更新動作失敗,JVM首先會檢查對象的Mark Word是否指向當前線程的棧幀,如果是就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標識的狀態值變為10,Mark Word中存儲的就是指向重量級鎖的指針,后面等待鎖的線程也要進入阻塞狀態。而當前線程變嘗試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞,而采用循環去獲取鎖的過程
偏向鎖、輕量級鎖與重量級鎖的對比
下面用一張表格來對比一下偏向鎖、輕量級鎖與重量級鎖,網上看到的,我覺得寫得非常好,為了加深記憶我自己又手打了一遍: