Java中對象的布局

前言

本文首發(fā)于spheign的博客網(wǎng)站,歡迎轉(zhuǎn)載。

1 概述

先說結(jié)論,Java對象保存在內(nèi)存中時(shí),由對象頭、實(shí)例數(shù)據(jù)、對對齊填充字節(jié)組成。
我們可以借助openjdk的jol-core包很方便的輸出對象布局。

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>
static class L{
    private final String str = "hello world";
}

public static void main(String[] args) {
    L l = new L();  //new 一個(gè)對象
    System.out.println(ClassLayout.parseInstance(l).toPrintable());//輸出 l對象 的布局
}
 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)                           94 ef 00 f8 (10010100 11101111 00000000 11111000) (-134156396)
     12     4   java.lang.String L.str                                     (object)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
  • OFFSET:偏移地址,單位字節(jié);
  • SIZE:占用的內(nèi)存大小,單位為字節(jié);
  • TYPE DESCRIPTION:類型描述,其中object header為對象頭;
  • VALUE:對應(yīng)內(nèi)存中當(dāng)前存儲(chǔ)的值;
    可以看出,對象頭所占用的內(nèi)存大小為12 * 8bit = 96bit,我是用的jdk版本是1.8,默認(rèn)開啟了指針壓縮??梢酝ㄟ^vm參數(shù)-XX:-UseCompressedOops進(jìn)行關(guān)閉,關(guān)閉后對象頭所占用的內(nèi)存大小為16 * 8bit = 128bit。
    關(guān)閉后輸出
 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)                           f0 fe 70 09 (11110000 11111110 01110000 00001001) (158400240)
     12     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     16     8   java.lang.String L.str                                     (object)
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

些許不同的是我們還要注意一下數(shù)組對象,將示例中的L替換成一個(gè)對象數(shù)組后:

static class L{
    private final String str = "hello world";
}

public static void main(String[] args) {
    L[] l = new L[7];  //new 一個(gè)對象
    System.out.println(ClassLayout.parseInstance(l).toPrintable());//輸出 l對象 的布局
}
  1. 關(guān)閉指針壓縮
 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)                           e8 60 57 07 (11101000 01100000 01010111 00000111) (123166952)
     12     4                           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     16     4                           (object header)                           07 00 00 00 (00000111 00000000 00000000 00000000) (7)
     20     4                           (alignment/padding gap)                  
     24    56   com.spheign.szjx.Test$L Test$L;.<elements>                        N/A
Instance size: 80 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total
  1. 開啟指針壓縮
 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)                           d3 ef 00 f8 (11010011 11101111 00000000 11111000) (-134156333)
     12     4                           (object header)                           07 00 00 00 (00000111 00000000 00000000 00000000) (7)
     16    28   com.spheign.szjx.Test$L Test$L;.<elements>                        N/A
     44     4                           (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

我們發(fā)現(xiàn),對象頭中多出來一行12 4 (object header) 07 00 00 00 (00000111 00000000 00000000 00000000) (7),這行代表的就是數(shù)組的長度,本例為7

我們隨便拿一條輸出來說明一下每一行代表什么意思。

 OFFSET  SIZE                      TYPE DESCRIPTION                                           VALUE
      0     4                           (object header)    // Mark Word                      01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                           (object header)    // Mark Word                      00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                           (object header)    // Klass Pointer 類元數(shù)據(jù)的指針     d3 ef 00 f8 (11010011 11101111 00000000 11111000) (-134156333)
     12     4                           (object header)    // 數(shù)組長度                        07 00 00 00 (00000111 00000000 00000000 00000000) (7)
     16    28   com.spheign.szjx.Test$L Test$L;.<elements> // Instance Data 對象實(shí)際的數(shù)據(jù)     N/A
     44     4                           (loss due to the next object alignment)  //Padding 對齊填充數(shù)據(jù)
Instance size: 48 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
object

2 對象頭

通過上面的學(xué)習(xí),我們能夠很容易的說出來對象頭的組成,Mark Word、類元數(shù)據(jù)的指針(Klass Pointer)、數(shù)組長度(不一定有)
而對象頭的重點(diǎn)內(nèi)容都在Mark Word上,它主要用來存儲(chǔ)對象自身的運(yùn)行時(shí)數(shù)據(jù),mark word的位長度為JVM的一個(gè)Word大小,也就是說32位JVM的Mark word為32位,64位JVM為64位。

下表是64位的情況

鎖狀態(tài) 分代年齡 (4bit) 是否偏向鎖 (1bit) 鎖標(biāo)志位 (2bit)
無鎖 unused (25bit) hashcode (31bit) unused (1bit) age (4bit) 0 01
偏向鎖 thread id (54bit) epoch (2bit) unused (1bit) age (4bit) 1 01
輕量級鎖 ptr_to_lock_record (62bit) 00
重量級鎖 ptr_to_heavyweight_monitor (62bit) 10
GC標(biāo)志 11
lock

2.1 鎖標(biāo)識

上圖中的內(nèi)存占用的順序正好可以對應(yīng)到我們前面代碼例子中的輸出,不過要轉(zhuǎn)換一下,我們把mark word的部分拿出來

 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)

VALUE部分是0x0100000000000000,將其倒敘0x0000000000000001,再轉(zhuǎn)換成二級制就正好對應(yīng)起來了。所以,我們看一個(gè)對象正好處于什么類型的鎖,我們只需要查看后三位就可以了。無鎖的情況是001我們已經(jīng)在上面的示例中展現(xiàn)出來了,這里我想把其他三種鎖的情況也都做一個(gè)示例輸出出來,很遺憾偏向鎖的示例我還沒有好的方案,所以這里就先展示輕量級鎖和重量級鎖。

static class L{
  private final Object lock = new Object();
  public void run(String name){
    synchronized (lock) {
      System.out.println(">>>>>>> thread name : " + name);
      System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
  }
}

public static void main(String[] args) {
  L l = new L();
  ExecutorService pool = Executors.newCachedThreadPool();
  pool.execute(()->{
    String threadName = Thread.currentThread().getName();
    l.run(threadName);
  });
  pool.shutdown();
}
java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           a8 d7 fa 03 (10101000 11010111 11111010 00000011) (66770856)
      4     4        (object header)                           00 70 00 00 (00000000 01110000 00000000 00000000) (28672)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

最后一位的二進(jìn)制位10101000,輕量級鎖。

我們再加一個(gè)線程

static class L{
  private final Object lock = new Object();
  public void run(String name){
    synchronized (lock) {
      System.out.println(">>>>>>> thread name : " + name);
      System.out.println(ClassLayout.parseInstance(lock).toPrintable());
    }
  }
}

public static void main(String[] args) {
  L l = new L();
  ExecutorService pool = Executors.newCachedThreadPool();
  pool.execute(()->{
    String threadName = Thread.currentThread().getName();
    l.run(threadName);
  });
  pool.execute(()->{
    String threadName = Thread.currentThread().getName();
    l.run(threadName);
  });
  pool.shutdown();
}
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           5a 11 03 a6 (01011010 00010001 00000011 10100110) (-1509748390)
      4     4        (object header)                           ce 7f 00 00 (11001110 01111111 00000000 00000000) (32718)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

最后一位的二進(jìn)制位01011010,重量級鎖。

這里有個(gè)重要的知識點(diǎn)叫鎖升級,我畫了一個(gè)圖來描述鎖升級的過程:

lock

有一個(gè)知識點(diǎn)我們必須要明確,synchronized鎖的是對象而不是其包裹的代碼。

  1. 對象被new出來后,沒有任何線程持有這個(gè)對象的鎖,這時(shí)就是無鎖狀態(tài);
  2. 當(dāng)且僅當(dāng)只有一個(gè)線程A獲取到這個(gè)對象的鎖的時(shí)候,對象就會(huì)從無鎖狀態(tài)升級成為偏向鎖,Mark Word中就會(huì)記錄這個(gè)線程的標(biāo)識,此時(shí)線程A持有這個(gè)對象的鎖;
  3. 還是這個(gè)線程A再次獲取這個(gè)對象的鎖時(shí),發(fā)現(xiàn)他是一個(gè)偏向鎖,并且對象頭中記錄著自己的線程標(biāo)識,那么線程A就繼續(xù)使用這把鎖。這里也是鎖的可重入性,所以,synchronized也是可重入鎖;
  4. 在線程A持有鎖的時(shí)候,線程B也來爭搶這把鎖了,線程B發(fā)現(xiàn)這是一把偏向鎖,并且對象頭中的線程標(biāo)識不是自己。那么,偏向鎖就會(huì)升級為輕量級鎖;
  5. 又有一些線程來爭搶這個(gè)輕量級鎖了,爭搶的過程其實(shí)就是利用CAS自旋。為了避免長時(shí)間自旋消耗CPU資源,當(dāng)自旋超過10次的時(shí)候,輕量級鎖升級為重量級鎖。

上面描述的就是鎖升級的過程,理論上鎖每升級一次,效率就會(huì)變差,但事實(shí)真是如此嗎?

  1. 偏向鎖的效率未必高。

    偏向鎖有鎖撤銷的操作,這個(gè)操作會(huì)消耗CPU資源,如果頻繁的進(jìn)行 無鎖--偏向鎖--無鎖 的轉(zhuǎn)換,還不如直接使用輕量級鎖。也就是只有一個(gè)線程頻繁的獲取鎖釋放鎖的過程。

  2. 輕量級鎖為什么要升級為重量級鎖?

    上面也說了,輕量級鎖在爭搶的時(shí)候會(huì)進(jìn)行自旋的操作,當(dāng)有許許多多的線程同時(shí)進(jìn)行自旋的時(shí)候,將相當(dāng)?shù)暮馁M(fèi)CPU資源??刂谱孕螖?shù)與時(shí)間也是CAS要做的優(yōu)化內(nèi)容。

  3. 重量級鎖為什么效率差?

    重量級鎖為OS級鎖,線程會(huì)進(jìn)入等待隊(duì)列中等待CPU的調(diào)用,因此,在進(jìn)行線程切換的時(shí)候會(huì)比較耗時(shí)。

  4. synchronized和Lock (CAS)應(yīng)該如何選擇?

    a. synchronized:高爭用、高耗時(shí)的場景,因?yàn)榈却?duì)列不消耗CPU資源;

    b. Lock (CAS):低爭用、低耗時(shí)的場景,因?yàn)榇藭r(shí)自旋次數(shù)很少就能拿到鎖;

    以上是理論情況,實(shí)際開發(fā)中一定要遵循實(shí)測的結(jié)果?。?!

2.2 分代年齡

我們平時(shí)遇見最多的問題就是分代年齡的最大值,顯而易見,分代年齡只有4bit所以的最大值也就是15。
分代年齡又叫GC分代年齡,他和垃圾回收機(jī)制有關(guān),GC回收的又是堆(Heap)空間的內(nèi)容,所以要理解分代年齡就要先搞清楚Heap空間。

2.2.1 堆Heap空間

在《深入理解Java虛擬機(jī)》一書中對以上內(nèi)容講解的非常清晰,建議大家都去讀一讀。
堆被分為三個(gè)區(qū)域,我畫了一張圖來直觀的說明一下


Java8

上圖的元空間并不適用與jdk1.7的版本,在jdk1.7中,元空間的位置是持久代。元空間使用的是本機(jī)物理內(nèi)存,而持久代使用的是JVM的堆內(nèi)存。

Java7

分區(qū)的目的就是為了優(yōu)化GC的性能,避免GC運(yùn)行時(shí)對整個(gè)Heap空間進(jìn)行掃描。Java對象中的絕大多數(shù)都是臨時(shí)對象,存活時(shí)間很短,分區(qū)后,只在很小的范圍內(nèi)掃描這些數(shù)據(jù)。

2.2.2 對象在堆空間中的生命周期(分代年齡的作用)

我們模擬一下對象分配空間的過程

  1. 新的對象在Eden區(qū)被new出來(大對象例外),一開始的時(shí)候兩個(gè)幸存者區(qū)域和老年區(qū)都是空的;

    1
  2. 對象創(chuàng)建的越來越多,Eden區(qū)域逐漸被填滿;

    2
  3. 此時(shí)將觸發(fā)Minor GC,刪除沒有引用的對象,沒有被刪除的對象被復(fù)制到From幸存區(qū),然后清空Eden區(qū)域;

    3
  4. 對象繼續(xù)創(chuàng)建,Eden區(qū)域又滿了,再一次觸發(fā)Minor G?,刪除沒有引用的對象,留下存在引用的對象,將這些對象和之前復(fù)制到From幸存區(qū)的對象一起復(fù)制到To幸存區(qū),然后清空Eden區(qū)和From區(qū)。這兩步也叫做GC的復(fù)制算法。

    4
  5. 對象繼續(xù)創(chuàng)建,Eden區(qū)域又滿了,第三次觸發(fā)Minor G?。與上次不同的是,To區(qū)From區(qū)將發(fā)生角色轉(zhuǎn)換,然后繼續(xù)執(zhí)行第四步。

    5
  6. 上面的操作其實(shí)已經(jīng)修改了分代年齡,Minor GC每發(fā)生一次,沒有被刪除的對象的分代年齡就會(huì)+1,直到達(dá)到分代年齡的閥值(默認(rèn)是15,由JVM參數(shù)MaxTenuringThreshold決定),這些對象就被移動(dòng)到老年區(qū)。

    6
  7. 當(dāng)老年區(qū)的存儲(chǔ)快滿了時(shí),將觸發(fā)Major GC,清理老年區(qū)沒有被引用的對象。

3 實(shí)例數(shù)據(jù)

并不是所有的變量都存放在這里,對象的的所有成員變量以及其父類的成員變量是存放在這里的。

也就是說,靜態(tài)變量和常量是不在這里面存儲(chǔ)的,它們被存放在方法區(qū)中。

這部分存儲(chǔ)的順序會(huì)受到虛擬機(jī)的分配策略參數(shù)(FieldsAllocationStyle)和字段在Java源碼中的定義順序影響。

4 對齊填充

JVM要求Java對象的大小必須是8byte的倍數(shù),所以這個(gè)的作用就是把對象的大小補(bǔ)齊至8byte的倍數(shù)。

注意:不是8bit(比特)的倍數(shù),是8bytes(字節(jié))的倍數(shù),1byte = 8bit。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容