Java對象在內(nèi)存中的布局 沒有你想的那么神秘

寫在前面

Java是用C++寫的夜焦,所以java對象最終會映射到c++中的某個對象,用這個對象可以描述所有Java對象晴竞。而我們所熟知的synchronized鎖的優(yōu)化就是基于這個對象來實(shí)現(xiàn)的泌射。

對象在內(nèi)存中的布局

Java對象在被創(chuàng)建的時候,在內(nèi)存分配完成后煮甥,虛擬機(jī)需要對對象進(jìn)行必要設(shè)置, 例如這個對象是哪個類的實(shí)例全跨、如何才能找到類的元數(shù)據(jù)信息缝左、對象的哈希碼、對象的GC分代年齡等信息。

這些信息存放在對象的對象頭(Object Header)中渺杉。根據(jù)虛擬機(jī)當(dāng)前運(yùn)行狀態(tài)的不同蛇数,如是否啟用偏向鎖等對象頭會有不同的設(shè)置方式。

在虛擬機(jī)中是越,對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭(Header)耳舅、實(shí)例數(shù)據(jù)(Instance Data)和對齊填充(Padding)

對象頭

HotSpot虛擬機(jī)對象頭包括兩部分信息,第一部分用于存儲對象自身的運(yùn)行時數(shù)據(jù)倚评,如哈希碼浦徊、GC分代年、鎖狀態(tài)標(biāo)志天梧、線程持有的鎖盔性、偏向線程ID、偏向時間戳呢岗。

這部分?jǐn)?shù)據(jù)的長度在32位和64位的虛擬機(jī)中分別是32bit和64bit,官方稱為"Mark Word"纯出。 考慮到虛擬機(jī)的空間效率,Mark Word被設(shè)計(jì)成一個非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲盡量多的信息敷燎。

對象頭另一部分是類型指針暂筝,即指向?qū)ο蟮念愒獢?shù)據(jù),虛擬機(jī)通過這個指針確定該對象是哪個類的實(shí)例硬贯。

我們可以在JVM源碼(hotspot/share/oops/markOop.hpp)中看到對象頭中存儲內(nèi)容的定義

class markOopDesc: public oopDesc {
 public:
  enum { age_bits             = 4,
         lock_bits            = 2,
         biased_lock_bits     = 1,
         max_hash_bits        = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits            = max_hash_bits > 31 ? 31 : max_hash_bits,
         cms_bits             = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits           = 2
  };
}
對象頭
  • hash: 對象的哈希碼
  • age: 對象的分代年齡
  • biased_lock : 偏向鎖標(biāo)識位
  • lock: 鎖狀態(tài)標(biāo)識位
  • JavaThread* : 持有偏向鎖的線程ID
  • epoch: 偏向時間戳

例如在32位的HotSpot虛擬機(jī)中焕襟,如果對象處于未被鎖定的狀態(tài)下,那么Mark Word的32bit空間的25bit用于存儲對象哈希碼饭豹,4bit用于存儲對象分代年齡鸵赖,2bit用于存儲鎖標(biāo)志位,1bit固定為0
而在其他狀態(tài)(輕量級鎖拄衰,重量級鎖它褪,GC標(biāo)記,可偏向)下對象的存儲內(nèi)容如下表所示

32位系統(tǒng)

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

實(shí)例數(shù)據(jù)部分是對象真正存儲的有效信息翘悉,也是在程序代碼中說定義的各種類型的字段內(nèi)容茫打。

對其填充

第三部分對其填充并不是必然存在的,也沒有特別的含義妖混,僅是占位符的作用老赤,因?yàn)镠otSpot VM的內(nèi)存管理系統(tǒng)要求對象起始地址必須是8字節(jié)的整數(shù)倍,換句話說制市,就是對象的大小必須是8字節(jié)的整數(shù)倍抬旺。而對象頭部分正好是8字節(jié)的倍數(shù),因此當(dāng)對象實(shí)例數(shù)據(jù)部分沒有對齊時祥楣,就需要通過對齊填充來補(bǔ)全开财。

查看對象頭

我們可以通過openjdk的jol工具來查看對象頭存儲的內(nèi)容汉柒,首先代碼如下

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

public class GetRange {
  // -XX:-UseCompressedOops
  public static void main(String[] args) {

    // 1字節(jié)=8位(1byte = 8bit)
    System.out.println(VM.current().details());

    MyClass myClass = new MyClass();

    ClassLayout classLayout = ClassLayout.parseInstance(myClass);

    System.out.println("****New Object****");

    System.out.println(classLayout.toPrintable());

    int hashCode = myClass.hashCode();

    System.out.println("MyClass hashCode : " + hashCode) ;
    System.out.println("MyClass hashCode 二進(jìn)制 " + Integer.toBinaryString(hashCode));
    System.out.println("MyClass hashCode 二進(jìn)制長度 " + Integer.toBinaryString(hashCode).length());

    System.out.println();

    System.out.println("****After invoke hashCode()****");

    System.out.println(classLayout.toPrintable(myClass));

    // 獲取系統(tǒng)字節(jié)序
    System.out.println("系統(tǒng)當(dāng)前字節(jié)序是:" + ByteOrder.nativeOrder());

  }
}

class MyClass {

  String name = "think123";

  int[] other;

  boolean status;
}

輸出內(nèi)容如下

jol-object-header.png

可以知道對象頭占了12個字節(jié),存在3個字節(jié)的對其填充。

jvm中使用oopDesc來描述一個對象

class oopDesc {
 private:
  volatile markOop _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;

}

我們可以看到對象頭被有兩部分责鳍,mark 部分竭翠,官方稱為 mark word,存儲的是哈希碼,對象分代年齡薇搁,偏向鎖標(biāo)志等信息斋扰。mark word長度是一個系統(tǒng)子寬,在64bit系統(tǒng)上是8個字節(jié)啃洋。

第二部分是klass的類型指針传货,指向這個對象是哪個類的實(shí)例,這里使用的是union這個聯(lián)合體,表示變量_klass_compressed_klass共享同一段內(nèi)存宏娄。未開啟指針壓縮時使用_klass问裕,開啟指針壓縮時使用_compressed_klass。narrowKlass實(shí)際上是一個32bit的unsigned int類型孵坚,因此占用4個字節(jié)粮宛,所以開啟指針壓縮后對象的頭部長度整體為12字節(jié)。

narrowKlass定義在hotspot/src/share/vm/oops/oopsHierarchy.hpp,它是juint類型卖宠,實(shí)際上是32bit的unsigned int

對象體:MyClass中定義了三個字段,int[],String都占用4個字節(jié)長度,boolean類型的占用了1個字節(jié)長度

填充部分:上面對象頭和對象體長度和為21字節(jié)巍杈,因?yàn)橐?字節(jié)對齊,因此需要填充3字節(jié)扛伍,這樣就剛好等于24字節(jié)筷畦。

當(dāng)我們關(guān)閉指針壓縮后(-XX:-UseCompressedOops),mark word占16個字節(jié),同時也采用8字節(jié)進(jìn)行對其刺洒。name和other兩個數(shù)據(jù)域分別占據(jù)8字節(jié)鳖宾。而開啟指針壓縮之后,這兩個字節(jié)分別占用4個字節(jié)逆航。
所以開啟這個選項(xiàng)是可以節(jié)約內(nèi)存的鼎文,從jdk8之后已經(jīng)默認(rèn)開啟此選項(xiàng)。

未開啟指針壓縮

上面我把當(dāng)前系統(tǒng)的字節(jié)序打印了出來因俐,可以看到當(dāng)前是小字節(jié)序拇惋。

舉例來說,數(shù)值0x2211使用兩個字節(jié)儲存:高位字節(jié)是0x22女揭,低位字節(jié)是0x11蚤假。
大端字節(jié)序:高位字節(jié)在前栏饮,低位字節(jié)在后吧兔,這是人類讀寫數(shù)值的方法。
小端字節(jié)序:低位字節(jié)在前袍嬉,高位字節(jié)在后境蔼,即以0x1122形式儲存灶平。

因此我們查看myClass的hashCode的時候就要倒著看了。我們查看object header中的hashcode要從第9位到39位(64位系統(tǒng)中,用31字節(jié)保存哈希)開始查看箍土。


myClass的hashcode(長度為30,最高位補(bǔ)2個0) :

myClass HashCode :    00100001 01011000 10001000 00001001

Mark Word中HashCode : 00001001 10001000 01011000 0 0100001

可以發(fā)現(xiàn)myClass的hashCode和mark word中的存儲剛好是反著來的逢享。

不是說存儲hash的只有31位嗎?為什么這里用32位來比較呢吴藻?我用32位來比較是為了更加易于觀察瞒爬。 實(shí)際上MarkWord中保存哈希碼最后8位的第一位0是從未使用的25位中借來的(需要結(jié)合小字節(jié)序)

接下來,我們使用JOL工具查看處于不同鎖狀態(tài)下沟堡,mark word中的標(biāo)志位是怎樣的侧但。

偏向鎖

首先我們來看偏向鎖,由于JVM默認(rèn)會在啟動后4秒才會啟動偏向鎖航罗,所以測試時需要設(shè)置馬上啟動偏向鎖(-XX:BiasedLockingStartupDelay=0)

// -XX:BiasedLockingStartupDelay=0
public static void main(String[] args) throws InterruptedException {

    Layouter layouter = new HotSpotLayouter(new X86_32_DataModel());

    MyClass myClass = new MyClass();

    ClassLayout layout = ClassLayout.parseInstance(myClass);

    System.out.println("進(jìn)入同步代碼塊之前:");
    System.out.println(layout.toPrintable());


    synchronized (myClass) {
      System.out.println("同步代碼塊中:");
      System.out.println(layout.toPrintable());
    }

    System.out.println("退出同步代碼塊后:");
    System.out.println(layout.toPrintable());

}

偏向鎖
  1. 可以看到在進(jìn)入同步代碼塊之前低8位是00000101,表示處于偏向鎖(后三位是101),但是現(xiàn)在還沒有偏向任何一個線程禀横,因此沒有數(shù)據(jù)
  2. 處于同步代碼塊中以及退出同步代碼塊時,mark word一致,低8位都是00000101,處于偏向鎖粥血,且存儲了偏向線程id以及時間戳等信息柏锄。說明退出同步塊后,依然保留偏向鎖的信息

輕量級鎖和重量級鎖

我們使用下面的代碼來演示輕量級鎖以及重量級時狀態(tài)位的變化情況

 //不設(shè)置立刻啟動偏向鎖
public static void main(String[] args) throws InterruptedException {

    Layouter layouter = new HotSpotLayouter(new X86_32_DataModel());

    MyClass myClass = new MyClass();

    ClassLayout layout = ClassLayout.parseInstance(myClass);

    System.out.println("創(chuàng)建t1線程之前:");
    System.out.println(layout.toPrintable());

    Thread t1 = new Thread(() -> {
       synchronized ((myClass)) {
         try {
             TimeUnit.SECONDS.sleep(5);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
       }
    });

    t1.start();

    System.out.println("持有鎖之前:");
    System.out.println(layout.toPrintable());


    synchronized (myClass) {
        System.out.println("持有鎖中:");
        System.out.println(layout.toPrintable());
    }

    System.out.println("釋放鎖:");
    System.out.println(layout.toPrintable());

    System.out.println("System.gc() 后");
    System.gc();
    System.out.println(layout.toPrintable());

}

運(yùn)行結(jié)果如下:

輕量級鎖和重量級鎖

mark word的狀態(tài)變更過程如下:

  1. 創(chuàng)建t1線程前階段复亏,鎖標(biāo)志位為01趾娃,偏向鎖標(biāo)志位為0,處于無鎖狀態(tài)
  2. main線程持有鎖之前階段缔御,標(biāo)志位為00茫舶,屬于輕量級鎖,此時t1線程已經(jīng)持有鎖且main線程未請求鎖刹淌,所以此時無競爭
  3. main線程持有鎖階段饶氏,標(biāo)志位為10,膨脹為重量級鎖有勾,此時t線程已經(jīng)已經(jīng)釋放了鎖疹启,main線程獲取鎖成功,即此時存在競爭
  4. main線程釋放鎖階段蔼卡,標(biāo)志位為10喊崖,仍為重量級鎖,不會自動降級
  5. System.gc后階段雇逞,恢復(fù)為無鎖狀態(tài)荤懂,GC年齡變?yōu)?

至此,我們知道了處于不同鎖情況下塘砸,狀態(tài)位的變化情況节仿。

寫到最后

創(chuàng)作不易,請大家點(diǎn)個贊呀掉蔬。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末廊宪,一起剝皮案震驚了整個濱河市矾瘾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌箭启,老刑警劉巖壕翩,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異傅寡,居然都是意外死亡放妈,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門荐操,熙熙樓的掌柜王于貴愁眉苦臉地迎上來大猛,“玉大人,你說我怎么就攤上這事淀零⊥旒ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵驾中,是天一觀的道長唉堪。 經(jīng)常有香客問我,道長肩民,這世上最難降的妖魔是什么唠亚? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮持痰,結(jié)果婚禮上灶搜,老公的妹妹穿的比我還像新娘。我一直安慰自己工窍,他們只是感情好割卖,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著患雏,像睡著了一般鹏溯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上淹仑,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天丙挽,我揣著相機(jī)與錄音,去河邊找鬼匀借。 笑死颜阐,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的吓肋。 我是一名探鬼主播凳怨,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蓬坡!你這毒婦竟也來了猿棉?” 一聲冷哼從身側(cè)響起磅叛,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤屑咳,失蹤者是張志新(化名)和其女友劉穎萨赁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體兆龙,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡杖爽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了紫皇。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片慰安。...
    茶點(diǎn)故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖聪铺,靈堂內(nèi)的尸體忽然破棺而出化焕,到底是詐尸還是另有隱情,我是刑警寧澤铃剔,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布撒桨,位于F島的核電站,受9級特大地震影響键兜,放射性物質(zhì)發(fā)生泄漏凤类。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一普气、第九天 我趴在偏房一處隱蔽的房頂上張望谜疤。 院中可真熱鬧,春花似錦现诀、人聲如沸夷磕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽企锌。三九已至,卻和暖如春于未,著一層夾襖步出監(jiān)牢的瞬間撕攒,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工烘浦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抖坪,地道東北人。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓闷叉,卻偏偏與公主長得像擦俐,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子握侧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評論 2 351