JAVA 對象頭分析及Synchronized鎖


對象內存布局

HotSpot虛擬機中,對象在內存中存儲的布局可以分為三塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

從上面的這張圖里面可以看出,對象在內存中的結構主要包含以下幾個部分:

  • Mark Word(標記字段):對象的Mark Word部分占4個字節,其內容是一系列的標記位,比如輕量級鎖的標記位,偏向鎖標記位等等。
  • Klass PointerClass對象指針):Class對象指針的大小也是4個字節,其指向的位置是對象對應的Class對象(其對應的元數據對象)的內存地址
  • 對象實際數據:這里面包括了對象的所有成員變量,其大小由各個成員變量的大小決定,比如:byteboolean是1個字節,shortchar是2個字節,intfloat是4個字節,longdouble是8個字節,reference是4個字節
  • 對齊:最后一部分是對齊填充的字節,按8個字節填充。

對象頭詳情

對象頭包括兩部分:Mark Word 和 類型指針。

標記字段(Mark Word)

MarkWord用於存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時間戳等等。

這部分數據的長度在32位和64位的虛擬機(暫不考慮開啟壓縮指針的場景)中分別為32個和64個bits。

對象需要存儲的運行時數據很多,其實已經超出了32、64位Bitmap結構所能記錄的限度,但是對象頭信息是與對象自身定義的數據無關的額外存儲成本,考慮到虛擬機的空間效率,Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲盡量多的信息,它會根據對象的狀態復用自己的存儲空間

例如在32位的HotSpot虛擬機中對象未被鎖定的狀態下,Mark Word32個bits空間中的25bits用於存儲對象哈希碼(HashCode),4bits用於存儲對象分代年齡,2bits用於存儲鎖標志位,1bit固定為0,在其他狀態(輕量級鎖定、重量級鎖定、GC標記、可偏向)下對象的存儲內容如下表所示。

32位標記字段詳情

|-------------------------------------------------------|--------------------|
|                  Mark Word (32 bits)                  |       State        |
|-------------------------------------------------------|--------------------|
| identity_hashcode:25 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|-------------------------------------------------------|--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|-------------------------------------------------------|--------------------|
|               ptr_to_lock_record:30          | lock:2 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
|               ptr_to_heavyweight_monitor:30  | lock:2 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
|                                              | lock:2 |    Marked for GC   |
|-------------------------------------------------------|--------------------|
  • lock:2位的鎖狀態標記位,由於希望用盡可能少的二進制位表示盡可能多的信息,所以設置了lock標記。

  • biased_lock:對象是否啟用偏向鎖標記,只占1個二進制位。為1時表示對象啟用偏向鎖,為0時表示對象沒有偏向鎖。

  • age4位的Java對象年齡。在GC中,如果對象在Survivor區復制一次,年齡增加1。當對象達到設定的閾值時,將會晉升到老年代。默認情況下,並行GC的年齡閾值為15,並發GC的年齡閾值為6由於age只有4位,所以最大值為15,這就是-XX:MaxTenuringThreshold選項最大值為15的原因。

  • identity_hashcode25位的對象標識Hash碼,采用延遲加載技術。調用方法System.identityHashCode()計算,並會將結果寫到該對象頭中。當對象被鎖定時,該值會移動到管程Monitor中。

  • thread:持有偏向鎖的線程ID

  • epoch:偏向時間戳。

  • ptr_to_lock_record:指向棧中鎖記錄的指針。

  • ptr_to_heavyweight_monitor:指向管程Monitor的指針。

64位標記字段詳情

|------------------------------------------------------------------------------|--------------------|
|                                  Mark Word (64 bits)                         |       State        |
|------------------------------------------------------------------------------|--------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 |       Normal       |
|------------------------------------------------------------------------------|--------------------|
| thread:54 |       epoch:2        | unused:1 | age:4 | biased_lock:1 | lock:2 |       Biased       |
|------------------------------------------------------------------------------|--------------------|
|                       ptr_to_lock_record:62                         | lock:2 | Lightweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                     ptr_to_heavyweight_monitor:62                   | lock:2 | Heavyweight Locked |
|------------------------------------------------------------------------------|--------------------|
|                                                                     | lock:2 |    Marked for GC   |
|------------------------------------------------------------------------------|--------------------|

類型指針(Klass Word)

類型指針指向對象的類元數據,虛擬機通過這個指針確定該對象是哪個類的實例。

這一部分用於存儲對象的類型指針,該指針指向它的類元數據,JVM通過這個指針確定對象是哪個類的實例。該指針的位長度為JVM的一個字大小,即32位的JVM32位,64位的JVM64位。
如果應用的對象過多,使用64位的指針將浪費大量內存,統計而言,64位的JVM將會比32位的JVM多耗費50%的內存。為了節約內存可以使用選項+UseCompressedOops開啟指針壓縮,其中,oopordinary object pointer普通對象指針。開啟該選項后,下列指針將壓縮至32位:

  • 每個Class的屬性指針(即靜態變量)
  • 每個對象的屬性指針(即對象變量)
  • 普通對象數組的每個元素指針

當然,也不是所有的指針都會壓縮,一些特殊類型的指針JVM不會優化,比如指向PermGenClass對象指針(JDK8中指向元空間的Class對象指針)、本地變量、堆棧元素、入參、返回值和NULL指針等。

數組長度(Array Length)

如果對象是一個數組,那么對象頭還需要有額外的空間用於存儲數組的長度。

這部分數據的長度也隨着JVM架構的不同而不同:32位的JVM上,長度為32位;64JVM則為64位。

64位JVM如果開啟+UseCompressedOops選項,該區域長度也將由64位壓縮至32位

使用JOL來分析java的對象布局

JOL簡介

JOL的全稱是Java Object Layout。是一個用來分析JVMObject布局的小工具。包括Object在內存中的占用情況,實例對象的引用情況等等。

JOL可以在代碼中使用,也可以獨立的以命令行中運行。命令行的我這里就不具體介紹了,今天主要講解怎么在代碼中使用JOL

使用JOL需要添加maven依賴:

      <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.14</version>
      </dependency>

查看分析vm信息

查看jdk版本

λ java -version
java version "1.8.0_271"
Java(TM) SE Runtime Environment (build 1.8.0_271-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.271-b09, mixed mode)

通過JOL查看jvm信息

public class ObjectHeadTest {
	public static void main(String[] args) {
		//查看字節序
		System.out.println(ByteOrder.nativeOrder());
		//打印當前jvm信息
		System.out.println("======================================");
		System.out.println(VM.current().details());
	}
}

輸出:

LITTLE_ENDIAN
======================================
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

上面的輸出中,我們可以看到:Objects are 8 bytes aligned,這意味着所有的對象分配的字節都是8的整數倍。

可以從上面的LITTLE_ENDIAN發現,內存中字節序使用的是小端模式。

  • 大端字節序:高位字節在前,低位字節在后,這是人類讀寫數值的方法。
  • 小端字節序:低位字節在前,高位字節在后,即以0x1122形式儲存。

計算機電路先處理低位字節,效率比較高,因為計算都是從低位開始的。所以,計算機的內部處理都是小端字節序。

人類還是習慣讀寫大端字節序。所以,除了計算機的內部處理,其他的場合幾乎都是大端字節序,比如網絡傳輸和文件儲存。

查看分析基本類型對象布局

分析String類型

System.out.println(ClassLayout.parseClass(String.class).toPrintable());

輸出:

java.lang.String object internals:
 OFFSET  SIZE     TYPE DESCRIPTION                               VALUE
      0    12          (object header)                           N/A
     12     4   char[] String.value                              N/A
     16     4      int String.hash                               N/A
     20     4          (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

先解釋下各個字段的含義

  • OFFSET是偏移量,也就是到這個字段位置所占用的byte數,
  • SIZE是后面類型的大小,
  • TYPE是Class中定義的類型,
  • DESCRIPTION是類型的描述,
  • VALUETYPE在內存中的值。

分析上面的輸出,我們可以得出,String類中占用空間的有5部分,第一部分是對象頭,占12個字節,第二部分是char數組,占用4個字節,第三部分是int表示的hash值,占4個字節 ,總共20個字節。但是JVM中對象內存的分配必須是8字節的整數倍,所以要補全4字節,最后String類的總大小是24字節。

分析Long類型

System.out.println(ClassLayout.parseClass(Long.class).toPrintable());

輸出:

java.lang.Long object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0    12        (object header)                           N/A
     12     4        (alignment/padding gap)                  
     16     8   long Long.value                                N/A
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

可以看到1個Long對象是占24個字節的,但是其中真正存儲longvalue只占8個字節。

分析Long實例對象

System.out.println(ClassLayout.parseInstance(Long.MAX_VALUE).toPrintable());

輸出:

java.lang.Long object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           05 23 00 f8 (00000101 00100011 00000000 11111000) (-134208763)
     12     4        (alignment/padding gap)                  
     16     8   long Long.value                                9223372036854775807
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

可以看出,對象實例的布局跟類型差不多

分析數組實例對象

	public static void main(String[] args) {
		//查看字節序
		List<String> arr = Lists.newArrayList();
		arr.add("111");
		arr.add("222");
		System.out.println(ClassLayout.parseInstance(arr).toPrintable());
		System.out.println("======================================");
		String[] strArr = {"0","1","2","3","4","5","6","7","8","9","10"};
		System.out.println(ClassLayout.parseInstance(strArr).toPrintable());
	}

輸出:

java.util.ArrayList object internals:
 OFFSET  SIZE                 TYPE DESCRIPTION                               VALUE
      0     4                      (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                      (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                      (object header)                           7e 2f 00 f8 (01111110 00101111 00000000 11111000) (-134205570)
     12     4                  int AbstractList.modCount                     2
     16     4                  int ArrayList.size                            2
     20     4   java.lang.Object[] ArrayList.elementData                     [(object), (object), null, null, null, null, null, null, null, null]
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

======================================
[Ljava.lang.String; object internals:
 OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
      0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                    (object header)                           43 37 00 f8 (01000011 00110111 00000000 11111000) (-134203581)
     12     4                    (object header)                           0b 00 00 00 (00001011 00000000 00000000 00000000) (11)
     16    44   java.lang.String String;.<elements>                        N/A
     60     4                    (loss due to the next object alignment)
Instance size: 64 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

可以發現arr是一個對象,對象頭長度為12bits,實例數據長度為12bits,分別是3個屬性,每個字符串為4bits

數組strArr是一個列表,對象頭長度為16bits,可以看到最后一個object header的二進制數據為1011,轉換成十進制是11,實例數據長度為44bits,每個字符串為4bits

上面都是字符串String,所有長度為4bits,如果改成其他類型,長度也會跟着變動,比如改成Long,就是變成每個8bits

分析HashMap外部引用

		HashMap hashMap= new HashMap();
		hashMap.put("flydean","www.flydean.com");
		System.out.println(GraphLayout.parseInstance(hashMap).toPrintable());

輸出:

java.util.HashMap@7106e68ed object externals:
          ADDRESS       SIZE TYPE                      PATH                           VALUE
        76bbcc048         48 java.util.HashMap                                        (object)
        76bbcc078         24 java.lang.String          .table[14].key                 (object)
        76bbcc090         32 [C                        .table[14].key.value           [f, l, y, d, e, a, n]
        76bbcc0b0         24 java.lang.String          .table[14].value               (object)
        76bbcc0c8         48 [C                        .table[14].value.value         [w, w, w, ., f, l, y, d, e, a, n, ., c, o, m]
        76bbcc0f8         80 [Ljava.util.HashMap$Node; .table                         [null, null, null, null, null, null, null, null, null, null, null, null, null, null, (object), null]
        76bbcc148         32 java.util.HashMap$Node    .table[14]                     (object)

Addresses are stable after 1 tries.

從結果我們可以看到HashMap本身是占用48字節的,它里面又引用了占用24字節的keyvalue

使用JOL可以分析java類和對象,這個對於我們對JVMjava源代碼的理解和實現都是非常有幫助的。

查看自定義類與實例的對象布局

public class ObjectHeadTest {

	private int intValue = 0;
	public Integer intValue2 = 999;
	private short s1=256;
	private Short s2=new Short("2222");
	private long l1=222222222222222L;
	private Long l2 = new Long(222222222222222L);
	public boolean isT = false;
	public Boolean isT2 = true;
	public byte b1=-128;
	public Byte b2=127;
	public char c1='a';
	public Character c2 = Character.MAX_VALUE;
	private float f1=22.22f;
	private Float f2=new Float("222.222");
	private double d1=22.222d;
	private Double d2 = new Double("2222.2222");
	private BigDecimal bigDecimal = BigDecimal.ONE;
	private String aa = "asdfasdfasdfasdfds";

	public static void main(String[] args) {
		ObjectHeadTest object = new ObjectHeadTest();
		//打印hashcode
		System.out.println(object.hashCode());
		//打印hashcode二進制
		System.out.println(Integer.toBinaryString(object.hashCode()));
		//打印hashcode十六進制
		System.out.println(Integer.toHexString(object.hashCode()));
		//查看字節序
		System.out.println("======================================");
		System.out.println(ClassLayout.parseClass(ObjectHeadTest.class).toPrintable());
		System.out.println("======================================");
		System.out.println(ClassLayout.parseInstance(object).toPrintable());
	}
}

輸出:

396873410
10111101001111100111011000010
17a7cec2
======================================
com.qhong.basic.jol.ObjectHeadTest object internals:
 OFFSET  SIZE                   TYPE DESCRIPTION                               VALUE
      0    12                        (object header)                           N/A
     12     4                    int ObjectHeadTest.intValue                   N/A
     16     8                   long ObjectHeadTest.l1                         N/A
     24     8                 double ObjectHeadTest.d1                         N/A
     32     4                  float ObjectHeadTest.f1                         N/A
     36     2                  short ObjectHeadTest.s1                         N/A
     38     2                   char ObjectHeadTest.c1                         N/A
     40     1                boolean ObjectHeadTest.isT                        N/A
     41     1                   byte ObjectHeadTest.b1                         N/A
     42     2                        (alignment/padding gap)                  
     44     4      java.lang.Integer ObjectHeadTest.intValue2                  N/A
     48     4        java.lang.Short ObjectHeadTest.s2                         N/A
     52     4         java.lang.Long ObjectHeadTest.l2                         N/A
     56     4      java.lang.Boolean ObjectHeadTest.isT2                       N/A
     60     4         java.lang.Byte ObjectHeadTest.b2                         N/A
     64     4    java.lang.Character ObjectHeadTest.c2                         N/A
     68     4        java.lang.Float ObjectHeadTest.f2                         N/A
     72     4       java.lang.Double ObjectHeadTest.d2                         N/A
     76     4   java.math.BigDecimal ObjectHeadTest.bigDecimal                 N/A
     80     4       java.lang.String ObjectHeadTest.aa                         N/A
     84     4                        (loss due to the next object alignment)
Instance size: 88 bytes
Space losses: 2 bytes internal + 4 bytes external = 6 bytes total

======================================
com.qhong.basic.jol.ObjectHeadTest object internals:
 OFFSET  SIZE                   TYPE DESCRIPTION                               VALUE
      0     4                        (object header)                           01 c2 ce a7 (00000001 11000010 11001110 10100111) (-1479622143)
      4     4                        (object header)                           17 00 00 00 (00010111 00000000 00000000 00000000) (23)
      8     4                        (object header)                           05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
     12     4                    int ObjectHeadTest.intValue                   0
     16     8                   long ObjectHeadTest.l1                         222222222222222
     24     8                 double ObjectHeadTest.d1                         22.222
     32     4                  float ObjectHeadTest.f1                         22.22
     36     2                  short ObjectHeadTest.s1                         256
     38     2                   char ObjectHeadTest.c1                         a
     40     1                boolean ObjectHeadTest.isT                        false
     41     1                   byte ObjectHeadTest.b1                         -128
     42     2                        (alignment/padding gap)                  
     44     4      java.lang.Integer ObjectHeadTest.intValue2                  999
     48     4        java.lang.Short ObjectHeadTest.s2                         2222
     52     4         java.lang.Long ObjectHeadTest.l2                         222222222222222
     56     4      java.lang.Boolean ObjectHeadTest.isT2                       true
     60     4         java.lang.Byte ObjectHeadTest.b2                         127
     64     4    java.lang.Character ObjectHeadTest.c2                         �
     68     4        java.lang.Float ObjectHeadTest.f2                         222.222
     72     4       java.lang.Double ObjectHeadTest.d2                         2222.2222
     76     4   java.math.BigDecimal ObjectHeadTest.bigDecimal                 (object)
     80     4       java.lang.String ObjectHeadTest.aa                         (object)
     84     4                        (loss due to the next object alignment)
Instance size: 88 bytes
Space losses: 2 bytes internal + 4 bytes external = 6 bytes total

分析對象頭

從上面的vm信息可以得出,該內存中的字節序為小端模式

hashcode的二進制位10111101001111100111011000010

拆分開來應該是10111 10100111 11001110 11000010

轉換成16進制17 a7 ce c2

64位的Mark Word中的布局為

unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2

按上述的展示為:

00 00 00 | 17 a7 ce c2| 01

反過來就是

01 | c2 ce a7 17 | 00 00 00

Synchronized三種鎖

Java對象的鎖狀態一共有四種,級別從低到高依次為: 無鎖(01) -> 偏向鎖(01) -> 輕量級鎖(00) -> 重量級鎖(10).

但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級。

JDK 1.6中默認是開啟偏向鎖和輕量級鎖的,我們也可以通過-XX:-UseBiasedLocking來禁用偏向鎖。

要注意鎖的升級目的是為了提高鎖的獲取效率和釋放效率

偏向鎖

引入偏向鎖的主要原因是,經過研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此為了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。

引入的主要目的是,為了在無多線程競爭的情況下盡量減少不必要的輕量級鎖執行路徑。因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由於一旦出現多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗必須小於節省下來的CAS原子指令的性能消耗)。

偏向鎖的核心思想是,如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提升程序的性能。所以,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。

但是對於鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,並不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。

獲取鎖

  1. 檢測Mark Word是否為可偏向狀態,即是否為偏向鎖1,鎖標識位為01
  2. 若為可偏向狀態,則測試線程ID是否為當前線程ID,如果是,則執行步驟(5),否則執行步驟(3);
  3. 如果線程ID不為當前線程ID,則通過CAS操作競爭鎖,替換ThreadID,競爭成功,則將Mark Word的線程ID替換為當前線程ID,否則執行線程(4);
  4. 通過CAS競爭鎖失敗,證明當前存在多線程競爭情況,嘗試撤銷偏向鎖,當到達全局安全點,獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后被阻塞在安全點的線程繼續往下執行同步代碼塊;
  5. 執行同步代碼塊

釋放鎖

偏向鎖的釋放采用了一種只有競爭才會釋放鎖的機制,線程是不會主動去釋放偏向鎖,需要等待其他線程來競爭。偏向鎖的撤銷需要等待全局安全點(這個時間點是上沒有正在執行的代碼)。其步驟如下:

  1. 暫停擁有偏向鎖的線程,判斷鎖對象是否還處於被鎖定狀態;
  2. 撤銷偏向鎖,恢復到無鎖狀態(01)或者輕量級鎖(00)的狀態;如果獲得偏向鎖的線程未活動或已退出同步代碼塊,直接將鎖對象頭設置為無鎖狀態如果為未退出同步代碼塊,就將獲取偏向鎖的線程掛起,偏向鎖升級為輕量級鎖,然后被掛起的線程再繼續執行代碼塊.

那么輕量級鎖和偏向鎖的使用場景為:

輕量級鎖是為了在線程交替執行同步塊時提高性能,

偏向鎖則是在只有一個線程執行同步塊時進一步提高性能。

偏向鎖流程圖

偏向鎖只在置換Thread ID的時候依賴了一次CAS原子指令,線程的進入,退出都不再通過CAS來加鎖解鎖

輕量級鎖

引入輕量級鎖的主要原因是,對絕大部分的鎖,在整個同步周期內都不存在競爭,可能是交替獲取鎖然后執行。(與偏向鎖的區別是,引入偏向鎖是假設同一個鎖都是由同一線程多次獲得,而輕量級鎖是假設同一個鎖是由n個線程交替獲得;相同點是都是假設不存在多線程競爭)
引入輕量級鎖的主要目的是,在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗(多指時間消耗)。
觸發輕量級鎖的條件是當關閉偏向鎖功能或者多個線程競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖,此時Mark Word的結構也變為輕量級鎖的結構。如果存在多個線程同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖

獲取鎖

  1. 判斷當前對象是否處於無鎖狀態(hashcode、0、01),若是,則JVM首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced前綴,即Displaced Mark Word);否則執行步驟(3);
  2. JVM利用CAS操作嘗試將對象的Mark Word更新為指向Lock Record的指針,,並將Lock Record里的owner指針指向對象的Mark Word。如果成功表示競爭到鎖,則將鎖標志位變成00(表示此對象處於輕量級鎖狀態),執行同步操作;如果失敗則執行步驟(3);
  3. 判斷當前對象的Mark Word是否指向當前線程的棧幀,如果是則表示當前線程已經持有當前對象的鎖,則直接執行同步代碼塊;否則自旋嘗試獲取,自旋到一定的次數,只能說明該鎖對象已經被其他線程搶占了,這時輕量級鎖需要膨脹為重量級鎖,鎖標志位變成10,后面等待的線程將會進入阻塞狀態;

釋放鎖

輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下:

  1. 使用CAS將鎖對象頭的Mark Word替換為線程棧幀中復制的Lock Record.
  2. 替換成功,線程離開同步代碼塊
  3. 替換失敗,說明有其他線程嘗試獲取該鎖,鎖已經膨脹為重量級鎖,同時喚醒 Monitor entry set 中被掛起的線程。

輕量級鎖流程圖

獲取鎖:

釋放鎖:

當前只有一個等待線程,則該線程通過自旋進行等待。但是當自旋超過一定的次數,或者一個線程在持有鎖,一個在自旋,又有第三個來訪時,輕量級鎖升級為重量級鎖.

重量級鎖

重量級鎖依賴對象內部的monitor鎖來實現,而monitor又依賴操作系統的MutexLock(互斥鎖)

Mutex變量的值為1,表示互斥鎖空閑,這個時候某個線程調用lock可以獲得鎖,而Mutex的值為0表示互斥鎖已經被其他線程獲得,其他線程調用lock只能掛起等待

獲取鎖時,鎖對象的Mark Word中存儲的是指向重量級鎖的指針,此時等待鎖的線程都會進入阻塞狀態。

我們經常看見的synchronized就是非常典型的重量級鎖,通過指令moniter enter 加鎖,moniter exit解鎖。

為什么重量級鎖開銷比較大

原因是當系統檢查到是重量級鎖之后,會把等待想要獲取鎖的線程阻塞,被阻塞的線程不會消耗CPU,但是阻塞或者喚醒一個線程,都需要通過操作系統來實現,也就是相當於從用戶態轉化到內核態,而轉化狀態是需要消耗時間的

內置鎖(ObjectMonitor)

Monitor可以理解為一個同步工具或一種同步機制,通常被描述為一個對象。每一個Java對象就有一把看不見的鎖,稱為內部鎖或者Monitor鎖。

通常所說的對象的內置鎖,是對象頭Mark Word中的重量級鎖指針指向的monitor對象,該對象是在HotSpot底層C++語言編寫的(openjdk里面看),簡單看一下代碼:

//結構體如下
ObjectMonitor::ObjectMonitor() {  
  _header       = NULL;  
  _count       = 0;  
  _waiters      = 0,  
  _recursions   = 0;       //線程的重入次數
  _object       = NULL;  
  _owner        = NULL;    //標識擁有該monitor的線程
  _WaitSet      = NULL;    //等待線程組成的雙向循環鏈表,_WaitSet是第一個節點
  _WaitSetLock  = 0 ;  
  _Responsible  = NULL ;  
  _succ         = NULL ;  
  _cxq          = NULL ;    //多線程競爭鎖進入時的單向鏈表
  FreeNext      = NULL ;  
  _EntryList    = NULL ;    //_owner從該雙向循環鏈表中喚醒線程結點,_EntryList是第一個節點
  _SpinFreq     = 0 ;  
  _SpinClock    = 0 ;  
  OwnerIsThread = 0 ;  
}  

ObjectMonitor隊列之間的關系轉換可以用下圖表示:
img

對象內置鎖ObjectMonitor流程

  • 所有期待獲得鎖的線程,在鎖已經被其它線程擁有的時候,這些期待獲得鎖的線程就進入了對象鎖的entry set區域。
  • 所有曾經獲得過鎖,但是由於其它必要條件不滿足而需要wait的時候,線程就進入了對象鎖的wait set區域 。
  • wait set區域的線程獲得Notify/notifyAll通知的時候,隨機的一個Thread(Notify)或者是全部的Thread(NotifyALL)從對象鎖的wait set區域進入了entry set中。
  • 在當前擁有鎖的線程釋放掉鎖的時候,處於該對象鎖的entryset區域的線程都會搶占該鎖,但是只能有任意的一個Thread能取得該鎖,而其他線程依然在entry set中等待下次來搶占到鎖之后再執行。

既然提到了_waitSet_EntryList(_cxq隊列后面會說),那就看一下底層的waitnotify方法

wait方法的實現過程:

  //1.調用ObjectSynchronizer::wait方法
void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) {
  /*省略 */
  //2.獲得Object的monitor對象(即內置鎖)
  ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj());
  DTRACE_MONITOR_WAIT_PROBE(monitor, obj(), THREAD, millis);
  //3.調用monitor的wait方法
  monitor->wait(millis, true, THREAD);
  /*省略*/
}
  //4.在wait方法中調用addWaiter方法
  inline void ObjectMonitor::AddWaiter(ObjectWaiter* node) {
  /*省略*/
  if (_WaitSet == NULL) {
    //_WaitSet為null,就初始化_waitSet
    _WaitSet = node;
    node->_prev = node;
    node->_next = node;
  } else {
    //否則就尾插
    ObjectWaiter* head = _WaitSet ;
    ObjectWaiter* tail = head->_prev;
    assert(tail->_next == head, "invariant check");
    tail->_next = node;
    head->_prev = node;
    node->_next = head;
    node->_prev = tail;
  }
}
  //5.然后在ObjectMonitor::exit釋放鎖,接着 thread_ParkEvent->park  也就是wait

總結:通過object獲得內置鎖(objectMonitor),通過內置鎖將Thread封裝成OjectWaiter對象,然后addWaiter將它插入以_waitSet為首結點的等待線程鏈表中去,最后釋放鎖。

notify方法的底層實現

  //1.調用ObjectSynchronizer::notify方法
    void ObjectSynchronizer::notify(Handle obj, TRAPS) {
    /*省略*/
    //2.調用ObjectSynchronizer::inflate方法
    ObjectSynchronizer::inflate(THREAD, obj())->notify(THREAD);
}
    //3.通過inflate方法得到ObjectMonitor對象
    ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
    /*省略*/
     if (mark->has_monitor()) {
          ObjectMonitor * inf = mark->monitor() ;
          assert (inf->header()->is_neutral(), "invariant");
          assert (inf->object() == object, "invariant") ;
          assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is inva;lid");
          return inf 
      }
    /*省略*/ 
      }
    //4.調用ObjectMonitor的notify方法
    void ObjectMonitor::notify(TRAPS) {
    /*省略*/
    //5.調用DequeueWaiter方法移出_waiterSet第一個結點
    ObjectWaiter * iterator = DequeueWaiter() ;
    //6.后面省略是將上面DequeueWaiter尾插入_EntrySet的操作
    /**省略*/
  }

總結:通過object獲得內置鎖(objectMonitor),調用內置鎖的notify方法,通過_waitset結點移出等待鏈表中的首結點,將它置於_EntrySet中去,等待獲取鎖。注意:notifyAll根據policy不同可能移入_EntryList或者_cxq隊列中,此處不詳談。

JVM中對鎖的優化

jdk1.6對鎖的實現引入了大量的優化,如自旋鎖適應性自旋鎖鎖消除鎖粗化偏向鎖輕量級鎖等技術來減少鎖操作的開銷。
鎖主要存在四中狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是為了提高獲得鎖和釋放鎖的效率。

自旋鎖

線程的阻塞和喚醒需要CPU從用戶態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作,勢必會給系統的並發性能帶來很大的壓力。同時我們發現在許多應用上面,對象鎖的鎖狀態只會持續很短一段時間,為了這一段很短的時間頻繁地阻塞和喚醒線程是非常不值得的。所以引入自旋鎖。

何謂自旋鎖?

所謂自旋鎖,就是讓該線程等待一段時間,不會被立即掛起,看持有鎖的線程是否會很快釋放鎖。怎么等待呢?執行一段無意義的循環即可(自旋)。

自旋等待不能替代阻塞,雖然它可以避免線程切換帶來的開銷,但是它占用了處理器的時間。如果持有鎖的線程很快就釋放了鎖,那么自旋的效率就非常好,反之,自旋的線程就會白白消耗掉處理的資源,它不會做任何有意義的工作,典型的占着茅坑不拉屎,這樣反而會帶來性能上的浪費。所以說,自旋等待的時間(自旋的次數)必須要有一個限度,如果自旋超過了定義的時間仍然沒有獲取到鎖,則應該被掛起。

自旋鎖在JDK 1.4.2中引入,默認關閉,但是可以使用-XX:+UseSpinning開開啟,在JDK1.6中默認開啟。同時自旋的默認次數為10次,可以通過參數-XX:PreBlockSpin來調整;

如果通過參數-XX:preBlockSpin來調整自旋鎖的自旋次數,會帶來諸多不便。假如我將參數調整為10,但是系統很多線程都是等你剛剛退出的時候就釋放了鎖(假如你多自旋一兩次就可以獲取鎖),你是不是很尷尬。於是JDK1.6引入自適應的自旋鎖,讓虛擬機會變得越來越聰明。

自適應自旋鎖

JDK 1.6引入了更加聰明的自旋鎖,即自適應自旋鎖。

所謂自適應就意味着自旋的次數不再是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。它怎么做呢?

線程如果自旋成功了,那么下次自旋的次數會更加多,因為虛擬機認為既然上次成功了,那么此次自旋也很有可能會再次成功,那么它就會允許自旋等待持續的次數更多。反之,如果對於某個鎖,很少有自旋能夠成功的,那么在以后要或者這個鎖的時候自旋的次數會減少甚至省略掉自旋過程,以免浪費處理器資源。
有了自適應自旋鎖,隨着程序運行和性能監控信息的不斷完善,虛擬機對程序鎖的狀況預測會越來越准確,虛擬機會變得越來越聰明。

鎖消除

為了保證數據的完整性,我們在進行操作時需要對這部分操作進行同步控制,但是在有些情況下,JVM檢測到不可能存在共享數據競爭,這是JVM會對這些同步鎖進行鎖消除。鎖消除的依據是逃逸分析的數據支持。
如果不存在競爭,為什么還需要加鎖呢?所以鎖消除可以節省毫無意義的請求鎖的時間。變量是否逃逸,對於虛擬機來說需要使用數據流分析來確定,但是對於我們程序員來說這還不清楚么?我們會在明明知道不存在數據競爭的代碼塊前加上同步嗎?但是有時候程序並不是我們所想的那樣?我們雖然沒有顯示使用鎖,但是我們在使用一些JDK的內置API時,如StringBufferVectorHashTable等,這個時候會存在隱形的加鎖操作。比如StringBufferappend()方法,Vectoradd()方法:

   public void vectorTest(){
        Vector<String> vector = new Vector<String>();
        for(int i = 0 ; i < 10 ; i++){
            vector.add(i + "");
        }

        System.out.println(vector);
    }復制代碼

在運行這段代碼時,JVM可以明顯檢測到變量vector沒有逃逸出方法vectorTest()之外,所以JVM可以大膽地將vector內部的加鎖操作消除。

鎖粗化

我們知道在使用同步鎖的時候,需要讓同步塊的作用范圍盡可能小—僅在共享數據的實際作用域中才進行同步,這樣做的目的是為了使需要同步的操作數量盡可能縮小,如果存在鎖競爭,那么等待鎖的線程也能盡快拿到鎖。
在大多數的情況下,上述觀點是正確的。但是如果一系列的連續加鎖解鎖操作,可能會導致不必要的性能損耗,所以引入鎖粗話的概念。
鎖粗話概念比較好理解,就是將多個連續的加鎖、解鎖操作連接在一起,擴展成一個范圍更大的鎖。如上面實例:vector每次add的時候都需要加鎖操作,JVM檢測到對同一個對象(vector)連續加鎖、解鎖操作,會合並一個更大范圍的加鎖、解鎖操作,即加鎖解鎖操作會移到for循環之外。

synchronized原理圖

參考

Java對象頭詳解

java頭的信息分析與三種鎖性能分析

java對象在內存中的結構(HotSpot虛擬機)

終於我用JOL打破了你對java對象的所有想象 | 程序那些事

JVM源碼分析之Java對象頭實現

Java 鎖與對象頭

輕量級鎖加鎖&解鎖過程

Java 中的偏向鎖、輕量級鎖和重量級鎖

死磕Synchronized底層實現

死磕 Java 並發 - 深入分析 synchronized 的實現原理

「阿里面試系列」Synchronized原理分析

不可不說的Java“鎖”事


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM