前言
本文首發(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對象 的布局
}
- 關(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
- 開啟指針壓縮
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

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 |

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è)圖來描述鎖升級的過程:

有一個(gè)知識點(diǎn)我們必須要明確,synchronized鎖的是對象而不是其包裹的代碼。
- 對象被new出來后,沒有任何線程持有這個(gè)對象的鎖,這時(shí)就是無鎖狀態(tài);
- 當(dāng)且僅當(dāng)只有一個(gè)線程A獲取到這個(gè)對象的鎖的時(shí)候,對象就會(huì)從無鎖狀態(tài)升級成為偏向鎖,Mark Word中就會(huì)記錄這個(gè)線程的標(biāo)識,此時(shí)線程A持有這個(gè)對象的鎖;
- 還是這個(gè)線程A再次獲取這個(gè)對象的鎖時(shí),發(fā)現(xiàn)他是一個(gè)偏向鎖,并且對象頭中記錄著自己的線程標(biāo)識,那么線程A就繼續(xù)使用這把鎖。這里也是鎖的可重入性,所以,synchronized也是可重入鎖;
- 在線程A持有鎖的時(shí)候,線程B也來爭搶這把鎖了,線程B發(fā)現(xiàn)這是一把偏向鎖,并且對象頭中的線程標(biāo)識不是自己。那么,偏向鎖就會(huì)升級為輕量級鎖;
- 又有一些線程來爭搶這個(gè)輕量級鎖了,爭搶的過程其實(shí)就是利用CAS自旋。為了避免長時(shí)間自旋消耗CPU資源,當(dāng)自旋超過10次的時(shí)候,輕量級鎖升級為重量級鎖。
上面描述的就是鎖升級的過程,理論上鎖每升級一次,效率就會(huì)變差,但事實(shí)真是如此嗎?
偏向鎖的效率未必高。
偏向鎖有鎖撤銷的操作,這個(gè)操作會(huì)消耗CPU資源,如果頻繁的進(jìn)行 無鎖--偏向鎖--無鎖 的轉(zhuǎn)換,還不如直接使用輕量級鎖。也就是只有一個(gè)線程頻繁的獲取鎖釋放鎖的過程。
輕量級鎖為什么要升級為重量級鎖?
上面也說了,輕量級鎖在爭搶的時(shí)候會(huì)進(jìn)行自旋的操作,當(dāng)有許許多多的線程同時(shí)進(jìn)行自旋的時(shí)候,將相當(dāng)?shù)暮馁M(fèi)CPU資源??刂谱孕螖?shù)與時(shí)間也是CAS要做的優(yōu)化內(nèi)容。
重量級鎖為什么效率差?
重量級鎖為OS級鎖,線程會(huì)進(jìn)入等待隊(duì)列中等待CPU的調(diào)用,因此,在進(jìn)行線程切換的時(shí)候會(huì)比較耗時(shí)。
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ū)域,我畫了一張圖來直觀的說明一下

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

分區(qū)的目的就是為了優(yōu)化GC的性能,避免GC運(yùn)行時(shí)對整個(gè)Heap空間進(jìn)行掃描。Java對象中的絕大多數(shù)都是臨時(shí)對象,存活時(shí)間很短,分區(qū)后,只在很小的范圍內(nèi)掃描這些數(shù)據(jù)。
2.2.2 對象在堆空間中的生命周期(分代年齡的作用)
我們模擬一下對象分配空間的過程
-
新的對象在Eden區(qū)被new出來(大對象例外),一開始的時(shí)候兩個(gè)幸存者區(qū)域和老年區(qū)都是空的;
1 -
對象創(chuàng)建的越來越多,Eden區(qū)域逐漸被填滿;
2 -
此時(shí)將觸發(fā)Minor GC,刪除沒有引用的對象,沒有被刪除的對象被復(fù)制到From幸存區(qū),然后清空Eden區(qū)域;
3 -
對象繼續(xù)創(chuàng)建,Eden區(qū)域又滿了,再一次觸發(fā)Minor G?,刪除沒有引用的對象,留下存在引用的對象,將這些對象和之前復(fù)制到From幸存區(qū)的對象一起復(fù)制到To幸存區(qū),然后清空Eden區(qū)和From區(qū)。這兩步也叫做GC的復(fù)制算法。
4 -
對象繼續(xù)創(chuàng)建,Eden區(qū)域又滿了,第三次觸發(fā)Minor G?。與上次不同的是,To區(qū)和From區(qū)將發(fā)生角色轉(zhuǎn)換,然后繼續(xù)執(zhí)行第四步。
5 -
上面的操作其實(shí)已經(jīng)修改了分代年齡,Minor GC每發(fā)生一次,沒有被刪除的對象的分代年齡就會(huì)+1,直到達(dá)到分代年齡的閥值(默認(rèn)是15,由JVM參數(shù)MaxTenuringThreshold決定),這些對象就被移動(dòng)到老年區(qū)。
6 當(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。





