一個對象占多少位元組? 關於對象的大小,對於C/C++來說,都是有sizeof函數可以直接獲取的,但是Java似乎沒有這樣的方法。不過還好,在JDK1.5之後引入了Instrumentation類,這個類提供了計算對象記憶體占用量的方法。至於具體Instrumentation類怎麼用就不說了,可以參看這 ...
一個對象占多少位元組?
關於對象的大小,對於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,以此類推
最後再說說元數據指針的大小。元數據指針是一個引用類型,因此正常來說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中存儲的就是指向重量級鎖的指針,後面等待鎖的線程也要進入阻塞狀態。而當前線程變嘗試使用自旋來獲取鎖,自旋就是為了不讓線程阻塞,而採用迴圈去獲取鎖的過程
偏向鎖、輕量級鎖與重量級鎖的對比
下麵用一張表格來對比一下偏向鎖、輕量級鎖與重量級鎖,網上看到的,我覺得寫得非常好,為了加深記憶我自己又手打了一遍: