寫在前面
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)容如下表所示
實(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)容如下
可以知道對象頭占了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());
}
- 可以看到在進(jìn)入同步代碼塊之前低8位是00000101,表示處于偏向鎖(后三位是101),但是現(xiàn)在還沒有偏向任何一個線程禀横,因此沒有數(shù)據(jù)
- 處于同步代碼塊中以及退出同步代碼塊時,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)變更過程如下:
- 創(chuàng)建t1線程前階段复亏,鎖標(biāo)志位為01趾娃,偏向鎖標(biāo)志位為0,處于無鎖狀態(tài)
- main線程持有鎖之前階段缔御,標(biāo)志位為00茫舶,屬于輕量級鎖,此時t1線程已經(jīng)持有鎖且main線程未請求鎖刹淌,所以此時無競爭
- main線程持有鎖階段饶氏,標(biāo)志位為10,膨脹為重量級鎖有勾,此時t線程已經(jīng)已經(jīng)釋放了鎖疹启,main線程獲取鎖成功,即此時存在競爭
- main線程釋放鎖階段蔼卡,標(biāo)志位為10喊崖,仍為重量級鎖,不會自動降級
- System.gc后階段雇逞,恢復(fù)為無鎖狀態(tài)荤懂,GC年齡變?yōu)?
至此,我們知道了處于不同鎖情況下塘砸,狀態(tài)位的變化情況节仿。
寫到最后
創(chuàng)作不易,請大家點(diǎn)個贊呀掉蔬。