首頁  >  文章  >  Java  >  Java虛擬機器14:Java物件大小、物件記憶體佈局及鎖定狀態變化

Java虛擬機器14:Java物件大小、物件記憶體佈局及鎖定狀態變化

巴扎黑
巴扎黑原創
2017-06-26 10:39:501053瀏覽

一個物件佔多少位元組?

關於物件的大小,對於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對於一般物件與陣列物件的大小計算方式有所不同,我畫了一張圖說明:

  1. #Mark Word:儲存物件運行時記錄訊息,佔用記憶體大小與機器位數一樣,即32位元機佔4位元組,64位元機佔8位元組

  2. #元資料指標:指向描述類型的Klass物件(Java類別的C++對等體)的指針,Klass物件包含了實例物件所屬類型的元數據,因此該欄位稱為元資料指針,JVM在運行時將頻繁使用這個指針定位到位於方法區內的類型資訊。這個資料的大小稍後說

  3. 陣列長度:陣列物件特有,一個指向int型的引用類型,用來描述陣列長度,這個資料的大小和元資料指標大小相同,同樣稍後說

  4. 實例資料:實例資料就是8大基本資料型別byte、short、int、long、float、double、char、boolean(物件型別也是由這8大基本資料型別複合而成),每個資料型別佔多少位元組就不一一例舉了

  5. 填充:不定,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物件的大小:

  1. #開啟指標壓縮時,8位元組Mark Word + 4字節元資料指標= 12字節,由於12位元組不是8的倍數,因此填入4字節,物件Object佔據16位元組記憶體

  2. ##關閉指標壓縮時,8位元組Mark Word + 8位元組元資料指標= 16位元組,由於16位元組剛好是8的倍數,因此不需要填滿位元組,物件Object佔據16位元組記憶體

#接著是字元'a'的大小:

  1. #開啟指標壓縮時,8位元組Mark Word + 4位元組元資料指標+ 1位元組char = 13字節,由於13位元組不是8的倍數,因此填入3字節,字元'a'佔據16位元組記憶體

  2. 關閉指標壓縮時,8位元組Mark Word + 8位元組元資料指標+ 1位元組char = 17位元組,由於17位元組不是8的倍數,因此填充7字節,字元'a'佔據24位元組記憶體

#接著是整數1的大小:

  1. #開啟指標壓縮時,8位元組Mark Word + 4位元組元資料指標+ 4位元組int = 16位元組,由於16位元組剛好是8的倍數,因此不需要填入位元組,整數型1佔據16位元組記憶體

  2. 關閉指標壓縮時,8位元組Mark Word + 8位元組元資料指標+ 4位元組int = 20字節,由於20位元組剛好是8的倍數,因此填入4字節,整數型1佔據24位元組記憶體

接著是字串"aaaaa"的大小,所有靜態字段不需要管,只關注實例字段,String物件中實例字段有"char value[]"與"int hash",由此可知:

  1. 開啟指標壓縮時,8位元組Mark Word + 4位元組元資料指標+ 4位元組引用+ 4位元組int = 20位元組,由於20位元組不是8的倍數,因此填滿4字節,字串"aaaaa"佔據24位元組記憶體

  2. #關閉指標壓縮時,8位元組Mark Word + 8位元組元資料指針+ 8位元組引用+ 4位元組int = 28位元組,由於28位元組不是8的倍數,因此填入4位元組,字串"aaaaa"佔據32位元組記憶體

最後是長度為1的char型陣列的大小:

  1. #開啟指標壓縮時,8位元組的Mark Word + 4位元組的元資料指標+ 4位元組的陣列大小引用+ 1位元組char = 17位元組,由於17位元組不是8的倍數,因此填入7位元組,長度為1的char型陣列佔據24字節記憶體

  2. 關閉指標壓縮時,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作者經過以往的研究發現大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代碼更低因此引入了偏向鎖。偏向鎖的取得過程為:

  1. 存取Mark Word中偏向鎖定的識別碼是否設定為1,所標誌位元是否為01----確認為可偏向狀態

  2. 如果為可偏向狀態,則測試線程id是否指向目前線程,如果是,執行(5),否則執行(3)

  3. 如果線程id並為指向當前線程,透過CAS操作競爭鎖定。如果競爭成功,則將Mark Word中的執行緒id設定為目前執行緒id,然後執行(5);如果競爭失敗,執行(4)

  4. 如果CAS取得偏向鎖定失敗,則表示有競爭。當達到全域安全點(safepoint)時獲得偏向鎖的執行緒被掛起,偏向鎖定升級為輕量級鎖定(因為偏向鎖定是假設沒有競爭,但是這裡出現了競爭,要對偏向鎖進行升級),然後被阻塞在安全點的執行緒繼續往下執行同步程式碼

  5. 執行同步程式碼

  1. 有獲取就有釋放,偏向鎖的釋放點在於上述的第(4)步,

  2. 只有遇到其他執行緒嘗試競爭偏向鎖定時,持有偏向鎖定的執行緒才會釋放鎖定
  3. ,執行緒不會主動去釋放偏向鎖定。偏向鎖定的釋放過程為:

需要等待全域安全點(在這個時間點上沒有字節碼正在執行)

#它會先暫停擁有偏向鎖定的線程,判斷鎖定物件是否處於被鎖定狀態

    偏向鎖定釋放後來恢復到未鎖定(識別位元為01)或輕量級鎖定(識別位元為00)狀態
  1.  

  2. 輕量級鎖定

  3. 輕量級鎖定的加鎖過程為:

  4. ##在程式碼進入同步區塊的時候,如果同步物件鎖定狀態為無鎖定狀態,JVM會先在目前執行緒的堆疊訊框中建立一個名為鎖定記錄(Lock Record)的空間,用於儲存鎖定物件目前的Mark Word的拷貝,官方稱為Displaced Mark Word,此時線程堆疊與物件頭的狀態如圖所示
  5. #拷貝物件頭中的Mark Word複製到鎖定記錄中
  6. 拷貝成功後,JVM將使用CAS操作嘗試將物件的Mark Word更新為指向Lock Record的指針,並將Lock Record裡的owner指標指向Object Mark Word,如果更新成功,則執行步驟(4),否則執行步驟(5)

#########如果更新動作成功,那麼當前執行緒就擁有了該物件的鎖,且物件Mark Word的鎖定標識位元設定為00,即表示此物件處於輕量級鎖定狀態,此時線堆疊與物件頭的狀態如圖所示###### ################如果更新動作失敗,JVM首先會檢查物件的Mark Word是否指向目前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步區塊繼續執行。否則說明多個線程競爭鎖,輕量級鎖就要膨脹為重量級鎖,鎖標識的狀態值變為10,Mark Word中存儲的就是指向重量級鎖的指針,後面等待鎖的線程也要進入阻塞狀態。而目前執行緒變嘗試使用自旋來取得鎖,自旋是為了不讓執行緒阻塞,而採用循環去取得鎖的過程############### ####

Comparison of biased locks, lightweight locks and heavyweight locks

The following uses a table to compare biased locks and lightweight locks Class locks and heavyweight locks, I saw it online and I think it is very well written. In order to deepen my memory, I typed it again by hand:

以上是Java虛擬機器14:Java物件大小、物件記憶體佈局及鎖定狀態變化的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn