1. 程式人生 > 實用技巧 >Synchronized(2):JVM物件結構

Synchronized(2):JVM物件結構

 1. 物件結構

  1.1物件結構概覽

      1. 物件頭:Instance Header

        Java物件最複雜的一部分,採用C++定義了頭的協議格式,儲存了Java物件hash、GC年齡、鎖標記、class指標、陣列長度等資訊,稍後做出詳細解說。

      2. 例項資料:Instance Data

        這部分資料才是真正具有業務意義的資料,實際上就是當前物件中的例項欄位。

        在VM中,物件的欄位是由基本資料型別和引用型別組成的。

        物件的欄位們有如下規則:

          1:除了物件整體需要按8位元組對齊外,每個成員變數都儘量使本身的大小在記憶體中儘量對齊。比如 int 按 4 位對齊,long 按 8 位對齊。

          2:類屬性按照如下優先順序進行排列:長整型long和雙精度型別double;整型int和浮點型float;字元char和短整型short;位元組型別byte和布林型別boolean;最後是引用型別。這些屬性都按照各自的單位對齊。

          3:優先按照規則一和二處理父類中的成員,接著才是子類的成員。

          4:當父類中最後一個成員和子類第一個成員的間隔如果不夠4個位元組的話,就必須擴充套件到4個位元組的基本單位。

          5:如果子類第一個成員是一個雙精度或者長整型,並且父類並沒有用完8個位元組,JVM會破壞規則2,按照整形(int),短整型(short),位元組型(byte),引用型別(reference)的順序,向未填滿的空間填充。

      3. 對齊填充(可能存在):

        在hotSpot虛擬機器中,預設的對齊位數是8,與CPU架構無關。

  1.2 物件的佔用空間

     1.2.1 理論分析

        物件佔用空間 =

          物件頭(

            Mark Word (32位JVM,4位元組)(64位JVM,8位元組)

            + Klass指標 (32位JVM,4位元組)(64位JVM,8位元組)(64位JVM && 預設開啟壓縮-XX:+UseCompressedOops,4位元組)

            + Array length (固定為4位元組,因為陣列最大長度是int最大值)

          )

            (Klass指標的大小根據是否開啟壓縮而定-XX:+UseCompressedOops)

          + 例項資料(

            + 物件內部的基本型別屬性:根據其大小而定比如 int=4位元組 。byte 1;boolean 1;char 2;short 2;int 4;float 4;long 8;double 8

            + 物件內部的引用型別屬性:只需要佔用其 Klass指標部分即可( 即4位元組或者8位元組)

          )

          + 對齊填充(

            補齊為8位元組的倍數

          )

        一個含有兩個元素的int陣列的佔用空間為:

          8(Mark Word)+

          4(Klass指標且開啟壓縮)+

          4(Array length )+

          4*2(兩個int)

          =24位元組

        一個字串物件佔用記憶體空間為:

        Mark Word (32位JVM,4位元組)(64位JVM,8位元組)+

        Klass指標 (32位JVM,4位元組)(64位JVM,8位元組)(64位JVM && 預設開啟壓縮-XX:+UseCompressedOops,4位元組)+

        1個 int 屬性(4位元組)+

        1個 long 屬性(8位元組)+

        char 陣列屬性(Mark Word 8位元組 + Klass指標4位元組 + Array length 固定為4位元組 + n個char佔用2n位元組)+

        對齊填充

          = 40位元組+2n位元組+對齊填充。    

          注意,說明一個問題,陣列在作為屬性時,佔用的空間並不只是Klass指標4位元組,而是陣列的佔用的全部記憶體?

     1.2.2列印顯示物件佔用空間方法1:

        使用Unsafe:

        java中的sun.misc.Unsafe類,有一個objectFieldOffset(Field f)方法,表示獲取指定欄位在所在例項中的起始地址偏移量,如此可以計算出指定的物件中每個欄位的偏移量,

          值為最大的那個就是最後一個欄位的首地址,加上該欄位的實際大小,就能知道該物件整體的大小。

         

1 class test{
2     boolean age;
3     int name;
4     boolean married;
5 }

 1 static sun.misc.Unsafe U = null;
 2 
 3 static Field theUnsafeInstance = null;
 4 
 5 static {
 6     try {
 7         theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
 8         theUnsafeInstance.setAccessible(true);
 9         U = (Unsafe) theUnsafeInstance.get(Unsafe.class);
10     } catch (NoSuchFieldException e) {
11         e.printStackTrace();
12     } catch (IllegalAccessException e) {
13         e.printStackTrace();
14     }
15 }

 1 public static void main(String[] args) {
 2     try {
 3         Field field1 = test.class.getDeclaredField("age");
 4         System.out.println(U.objectFieldOffset(field1));
 5         Field field2 = test.class.getDeclaredField("name");
 6         System.out.println(U.objectFieldOffset(field2));
 7         Field field3 = test.class.getDeclaredField("married");
 8         System.out.println(U.objectFieldOffset(field3));
 9         Field field4 = test.class.getDeclaredField("hh");
10         System.out.println(U.objectFieldOffset(field4));
11 
12     } catch (NoSuchFieldException e) {
13         e.printStackTrace();
14     }
15 
16 }

1 16
2 12
3 17
4 java.lang.NoSuchFieldException: hh
5     at java.lang.Class.getDeclaredField(Class.java:2070)
6     at test.main(test.java:46)

        可以看到,通過Unsafe.objectFieldOffset(fieldName)方法計算看到:

        test類的age/name/married三個欄位的起始偏移量分別是16、12、17

        而又因為married是boolean型別(見1.3.0),這個boolean欄位排在在例項資料的最後,所以married欄位的結尾偏移量為17+1。

        所以可以算出每個test類物件佔用空間大小 = 18 + 6= 24位元組.(6位元組是padding)

     1.2.3 列印物件佔用空間方法2:

        見下文《1.4 列印各種鎖物件》。

  1.3 物件、物件頭詳解

     1.3.0 OOP-Klass模型

        JVM中把我們上層可見的Java物件在底層實際上表示為兩部分:oop部分 + Klass部分,

        Oop儲存在堆上,專注於表示物件的資訊,就是通常意義上的堆中物件。

          (物件頭、例項資料、對其填充)

        Klass儲存在方法區,專注於表示類的資訊。(所以儲存在方法區)

          猜測方法區的內部結構以類為單位的一坨一坨的塊。這麼看應該就是Klass了。所以Klass儲存的資訊 = 方法區儲存的資訊。

          (型別資訊、執行時常量池、欄位資訊、方法資訊等)

                

     1.3.1 物件之OOP

        當我們基於某個Java類用new建立一個新物件時,JVM會為其在堆上建立一個相應的oopDesc類的子類(instanceOopDesc)例項,用於存放該物件的(物件頭、例項資料、對其填充)。

        注意和Klass的定義區分,Klass是類載入時生成、用於存放該類的東西即metadata。

        而在堆上建立的這個instanceOopDesc所對應的地址會被用來建立一個引用,賦給當前執行緒執行時棧上的一個變數。

        1.3.1.1 oopDesc類:

          所在位置:oop.hpp檔案。

          是所有Oop的共同基類,

          這個類的內部結構就是通常意義上的堆中物件的結構:物件頭、 例項資料、對齊填充。

     1.3.2 物件之Klass

     1.3.3 物件之Oop的程式碼實現(即oopDesc類的C++程式碼結構,即oop.hpp檔案)

     1.3.4 物件之Klass的程式碼實現(即InstanceKlass類的C++程式碼結構,即InstanceKlass.hpp檔案)

     1.3.5 物件頭之_klass

     1.3.6 物件頭之markword

     1.3.7 物件頭之各個子部分實現

        1.3.7.1 物件頭之各個子部分實現(64位環境 + 未開啟指標壓縮)

        1.3.7.2 物件頭之各個子部分實現(64位環境 + 開啟指標壓縮)

        1.3.7.3 物件頭之各個子部分實現(32位環境)

 1.4 列印各種鎖物件

     1.4.1 前置條件:

     1.4.2 列印:無鎖物件:

     1.4.3 列印:匿名偏向鎖物件:

        (需要上來就把執行緒sleep一會兒,sleep時長為“偏向鎖延遲載入時間”,目的是啟動偏向鎖,即當前程式所有物件的初始狀態都是匿名偏向鎖物件)

     1.4.4 列印:偏向執行緒的偏向鎖物件:

        (在匿名偏向鎖的基礎上,再加個synchronized )

     1.4.5 列印:從無鎖直接升級為輕量鎖物件:

        (跳過匿名偏向鎖,趁著巨集程式還未開啟偏向鎖時,就加上synchronized )

     1.4.6 列印:從偏向鎖升級為輕量鎖物件:

        (匿名偏向鎖 + 兩個以上執行緒競爭 + 利用join實現不同時競爭一個物件)

     1.4.7 列印:重量鎖物件:

        (匿名偏向鎖 + 兩個以上執行緒競爭 + 同時競爭一個物件)

     1.4.8 總結一下各種鎖的建立場景: