64位JVM的Java對象頭詳解,從hotspot源碼中尋找答案

咱們編寫一個Java類,編譯後會生成.class文件,當類加載器將class文件加載到jvm時,會生成一個Klass類型的對象(c++),稱爲類描述元數據,存儲在方法區中,即jdk1.8以後的元數據區。當使用new建立對象時,就是根據類描述元數據Klass建立的對象oop,存儲在堆中。每一個java對象都有相同的組成部分,稱爲對象頭。java

在學習併發編程知識synchronized時,咱們老是難以理解其實現原理,由於偏向鎖、輕量級鎖、重量級鎖都涉及到對象頭,因此瞭解java對象頭是咱們深刻了解synchronized的前提條件。ios

查看對象頭的神器

介紹一款能夠在代碼中計算java對象的大小以及查看java對象內存佈局的工具包:jol-corejoljava object layout的縮寫,即java對象佈局。使用只須要到maven倉庫http://mvnrepository.com搜索java object layout,選擇想要使用的版本,將依賴添加到項目中便可。c++

使用jol計算對象的大小(單位爲字節):編程

ClassLayout.parseInstance(obj).instanceSize() 
複製代碼

使用jol查看對象的內存佈局:數組

ClassLayout.parseInstance(obj).toPrintable()
複製代碼

使用JOL查看對象的內存佈局

網絡搜索了不少資料,對64jvmJava對象頭的佈局講解的都很模糊,不少資料都是講的32位,並且不少都是從一些書上摘抄下來的,不免會存在錯誤的地方,因此最好的學習方法就是本身去驗證,看jvm源碼。本篇將詳細介紹64jvmJava對象頭。bash

User類爲例網絡

public class User {
    private String name;
    private Integer age;
    private boolean sex;
}
複製代碼

經過jol查看User對象的內存佈局併發

User user = new User()
System.out.println(ClassLayout.parseInstance(user).toPrintable());
複製代碼

輸出內容以下 jvm

  • OFFSET: 偏移地址,單位字節;
  • SIZE:佔用的內存大小,單位爲字節;
  • TYPE DESCRIPTION:類型描述,其中object header爲對象頭;
  • VALUE:對應內存中當前存儲的值;

從圖中能夠看到,對象頭所佔用的內存大小爲16*8bit=128bit。若是你們本身動手去打印輸出,可能獲得的結果是96bit,這是由於我關閉了指針壓縮。jdk8版本是默認開啓指針壓縮的,能夠經過配置vm參數關閉指針壓縮。maven

-XX:-UseCompressedOops
複製代碼

如今取消關閉指針壓縮的配置,開啓指針壓縮以後,再看User對象的內存佈局。

開啓指針壓縮能夠減小對象的內存使用。從兩次打印的User對象佈局信息來看,關閉指針壓縮時,name字段和age字段因爲是引用類型,所以分別佔8個字節,而開啓指針壓縮以後,這兩個字段只分別佔用4個字節。所以,開啓指針壓縮,理論上來說,大約能節省百分之五十的內存。jdk8及之後版本已經默認開啓指針壓縮,無需配置。

從兩次打印的User對象的內存佈局,還能夠看出,bool類型的age字段只佔用1個字節,但後面會跟隨幾個字節的浪費,即內存對齊。開啓指針壓縮狀況下,age字段的內存對齊須要3個字節,而關閉指針壓縮狀況下,則須要7個字節。

以默認開啓指針壓縮狀況下的User對象的內存佈局來看,對象頭佔用12個字節,那麼這12個字節存儲的是什麼信息,咱們不看網上的資料,而是看jdk的源碼。

openjdk源碼下載或在線查看

官網:openjdk.java.net/

我當前使用的jdk版本是jdk1.8,可經過命令行java -version查看,也可經過下面方式查看,這個你們應該都很熟悉了。

System.out.println(System.getProperties());
複製代碼

打開官網後點擊左側菜單欄的Groups找到HotSpot,在打開的頁面的Source code選擇Browsable souce,以後會跳轉到hg.openjdk.java.net/,選擇jdk8u,跳轉後的頁面中繼續選擇jdk8u下面的hotspot。傳送連接:hg.openjdk.java.net/jdk8u/jdk8u…

  • 點擊zip可將源碼打包下載;
  • 點擊browse可在線查看源碼;

Java對象頭詳解

在開啓指針壓縮的狀況下,User對象的對象頭佔用12個字節,本節咱們經過源碼瞭解對象頭都存儲了哪些信息。

Java程序運行的過程當中,每建立一個新的對象,JVM就會相應地建立一個對應類型的oop對象,存儲在堆中。如new User(),則會建立一個instanceOopDesc,基類爲oopDesc

[instanceOop.hpp文件:hotspot/src/share/vm/oops/instanceOop.hpp]
class instanceOopDesc : public oopDesc {
}
複製代碼

instanceOopDesc只提供了幾個靜態方法,如獲取對象頭大小。所以重點看其父類oopDesc

[oop.hpp文件:hotspot/src/share/vm/oops/oop.hpp]
class oopDesc {
   friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
  ........
}
複製代碼

咱們只關心對象頭,普通對象(如User對象,本篇不講數組類型)的對象頭由一個markOop和一個聯合體組成,markOop就是MarkWord。這個聯合體是指向類的元數據指針,未開啓指針壓縮時使用_klass,開啓指針壓縮時使用_compressed_klass

markOopnarrowKlass的類型定義在/hotspot/src/share/vm/oops/oopsHierarchy.hpp頭文件中:

[oopsHierarchy.hpp頭文件:/hotspot/src/share/vm/oops/oopsHierarchy.hpp]
typedef juint  narrowKlass;
typedef class markOopDesc* markOop;
複製代碼

所以,narrowKlass是一個juintjunit是在globalDefinitions_visCPP.hpp頭文件中定義的,這是一個無符號整數,即4個字節。因此開啓指針壓縮以後,指向Klass對象的指針大小爲4字節。

[/hotspot/src/share/vm/utilties/globalDefinitions_visCPP.hpp]
typedef unsigned int juint;
複製代碼

markOop則是markOopDesc類型指針,markOopDesc就是MarkWord。不知道大家有沒有感受到奇怪,在64jvm中,markOopDesc指針是8字節,即64bit,確實恰好是MarkWord的大小,可是指針指向的不是一個對象嗎?咱們先看markOopDesc類。

[markOop.hpp文件:hotspot/src/share/vm/oops/markOop.hpp]
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
class markOopDesc: public oopDesc {
 ......
}
複製代碼

markOop.hpp頭文件中給出了64bitMarkWord存儲的信息說明。markOopDesc類也繼承oopDesc。若是單純的看markOopDesc類的源碼,根本找不出來,markOopDesc是用那個字段存儲MarkWord的。並且,根據從各類來源的資料中,咱們所知道的是,對象頭的前8個字節存儲的就是是否偏向鎖、輕量級鎖等等信息(全文都是以64位爲例),因此不該該是個指針啊。

爲了解答這個疑惑,我是先從markOopDesc類的源碼中,找一個方法,好比,獲取gc對象年齡的方法,看下jvm是從哪裏獲取的數據。

class markOopDesc: public oopDesc {
public:
 // 獲取對象年齡
  uint age() const { 
    return mask_bits(value() >> age_shift, age_mask); 
  }
  // 更新對象年齡
  markOop set_age(uint v) const {
    return markOop((value() & ~age_mask_in_place) | (((uintptr_t)v & age_mask) << age_shift));
  }
  // 自增對象年齡
  markOop incr_age() const { 
    return age() == max_age ? markOop(this) : set_age(age() + 1); 
  }
}
複製代碼

那麼,value()這個方法返回的就是64bitMarkWord了。

class markOopDesc: public oopDesc {
 private:
  // Conversion
  uintptr_t value() const { return (uintptr_t) this; }
}
複製代碼

value方法返回的是一個指針,就是this。從set_ageincr_age方法中也能夠看出,只要修改MarkWord,就會返回一個新的markOopmarkOopDesc*)。難怪會將markOopDesc*定義爲markOop,就是將markOopDesc*當成一個8字節的整數來使用。想要理解這個,咱們須要先補充點c++知識,所以我寫了個demo

自定義一個類叫oopDesc,而且除構造函數和析構函數以外,只提供一個Show方法。

[.hpp文件]
#ifndef oopDesc_hpp
#define oopDesc_hpp

#include <stdio.h>

#include <iostream>
using namespace std;

// 將oopDesc* 定義爲 oop
typedef class oopDesc* oop;

class oopDesc{
public:
    void Show();
};
#endif /* oopDesc_hpp */

[.cpp文件]
#include "oopDesc.hpp"

void oopDesc::Show(){
    cout << "oopDesc by wujiuye" <<endl;
}
複製代碼

使用oop(指針)建立一個oopDesc*,並調用show方法。

#include <iostream>
#include "oopDesc.hpp"
using namespace std;
int main(int argc, const char * argv[]) {
    // 
    oopDesc* o = oop(0x200);
    cout << o << endl;
    o->Show();
    // 
    oopDesc* o1;
    o1->Show();
    return 0;
}
複製代碼

測試輸出

0x200
oopDesc by wujiuye
oopDesc by wujiuye
Program ended with exit code: 0
複製代碼

所以,經過類名(value)能夠建立一個野指針對象,將指針賦值爲value,這樣就可使用this做爲MarkWord了。若是在oopDesc中添加一個字段,並提供一個方法訪問,程序運行就會報錯,所以,這樣建立的對象只能調用方法,不能訪問字段。

MarkWord

鎖狀態/gc Mark Word (64 bits) - - - 是否偏向鎖 鎖標誌位
無鎖 unused:25 hashcode:31 unused:1 age:4 biased_lock:1 lock:2
偏向鎖 thread:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2
輕量級鎖 ptr_to_lock_record:62 - - - - lock:2
重量級鎖 ptr_to_heavyweight_monitor:62 - - - - lock:2
gc標誌 - - - - - lock:2

經過倒數三位判斷當前MarkWord的狀態,就能夠判斷出其他位存儲的是什麼。

[markOop.hpp文件]
enum {  locked_value             = 0, // 0 00 輕量級鎖
         unlocked_value           = 1,// 0 01 無鎖
         monitor_value            = 2,// 0 10 重量級鎖
         marked_value             = 3,// 0 11 gc標誌
         biased_lock_pattern      = 5 // 1 01 偏向鎖
  };
複製代碼

如今,咱們再看下User對象打印的內存佈局。

經過前面的講解,咱們已經知道,對象頭的前 64位是 MarkWord,後 32位是類的元數據指針(開啓指針壓縮)。

從圖中能夠看出,在無鎖狀態下,該User對象的hashcode0x7a46a697。因爲MarkWord實際上是一個指針,在64jvm下佔8字節。所以MarkWordk0x0000007a46a69701,跟你從圖中看到的正好相反。這裏涉及到一個知識點「大端存儲與小端存儲」。

  • Little-Endian:低位字節存放在內存的低地址端,高位字節存放在內存的高地址端。
  • Big-Endian:高位字節存放在內存的低地址端,低位字節存放在內存的高地址端。

學過彙編語言的朋友,這個知識點應該都還記得。本篇不詳細介紹,不是很明白的朋友能夠網上找下資料看。

寫一個synchronized加鎖的demo分析鎖狀態

接着,咱們再看一下,使用synchronized加鎖狀況下的User對象的內存信息,經過對象頭分析鎖狀態。

案例1

public class MarkwordMain {

    private static final String SPLITE_STR = "===========================================";
    private static User USER = new User();

    private static void printf() {
        System.out.println(SPLITE_STR);
        System.out.println(ClassLayout.parseInstance(USER).toPrintable());
        System.out.println(SPLITE_STR);
    }

    private static Runnable RUNNABLE = () -> {
        while (!Thread.interrupted()) {
            synchronized (USER) {
                printf();
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(RUNNABLE).start();
            Thread.sleep(1000);
        }
        Thread.sleep(Integer.MAX_VALUE);
    }
}
複製代碼

從該對象頭中分析加鎖信息,MarkWordk0x0000700009b96910,二進制爲0xb00000000 00000000 01110000 00000000 00001001 10111001 01101001 00010000

倒數第三位爲"0",說明不是偏向鎖狀態,倒數兩位爲"00",所以,是輕量級鎖狀態,那麼前面62位就是指向棧中鎖記錄的指針。

案例2

public class MarkwordMain {

    private static final String SPLITE_STR = "===========================================";
    private static User USER = new User();

    private static void printf() {
        System.out.println(SPLITE_STR);
        System.out.println(ClassLayout.parseInstance(USER).toPrintable());
        System.out.println(SPLITE_STR);
    }

    private static Runnable RUNNABLE = () -> {
        while (!Thread.interrupted()) {
            synchronized (USER) {
                printf();
            }
        }
    };

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(RUNNABLE).start();
        }
        Thread.sleep(Integer.MAX_VALUE);
    }
}
複製代碼

從該對象頭中分析加鎖信息,MarkWordk0x0000700009b96910,二進制爲0xb00000000 00000000 01111111 11110000 11001000 00000000 01010011 11101010

倒數第三位爲"0",說明不是偏向鎖狀態,倒數兩位爲"10",所以,是重量級鎖狀態,那麼前面62位就是指向互斥量的指針。

相關文章
相關標籤/搜索