jvm-對(duì)象內(nèi)存布局
對(duì)象內(nèi)存結(jié)構(gòu)概述
對(duì)象的創(chuàng)建過(guò)程:
- jvm將對(duì)象所在的class文件加載到方法區(qū)中
- jvm讀取main方法入口逛艰,將main方法入棧诈茧,執(zhí)行創(chuàng)建對(duì)象代碼
- 在main方法的棧內(nèi)存中分配對(duì)象的引用,在堆中分配內(nèi)存放入創(chuàng)建的對(duì)象云挟,并將棧中的引用指向堆中的對(duì)象
堆內(nèi)存中的對(duì)象由3部分組成:
- 對(duì)象頭 header
- 實(shí)例數(shù)據(jù) instance data
- 對(duì)齊填充字節(jié) padding
對(duì)象頭
對(duì)象頭存儲(chǔ)的是對(duì)象在運(yùn)行時(shí)狀態(tài)的相關(guān)信息姑原、指向該對(duì)象所屬類(lèi)的元數(shù)據(jù)的指針岭妖,如果對(duì)象是數(shù)組對(duì)象那么還會(huì)額外存儲(chǔ)對(duì)象的數(shù)組長(zhǎng)度
對(duì)象頭的組成
普通對(duì)象:
- 標(biāo)記字 mark word
- 指針類(lèi)型 klass pointer
數(shù)組對(duì)象:
- 標(biāo)記字 mark word
- 指針類(lèi)型 klass pointer
- 數(shù)組長(zhǎng)度
mark word
在對(duì)象頭中徽曲,mark word 一共有64個(gè)bit零截,用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),標(biāo)記對(duì)象處于以下5種狀態(tài)中的某一種:
(此處應(yīng)有圖)
在mark word中秃臣,鎖(lock)標(biāo)志位占用2個(gè)bit涧衙,結(jié)合1個(gè)bit偏向鎖(biased_lock)標(biāo)志位,這樣通過(guò)倒數(shù)的3位甜刻,就能用來(lái)標(biāo)識(shí)當(dāng)前對(duì)象持有的鎖的狀態(tài)绍撞,并判斷出其余位存儲(chǔ)的是什么信息。
基于mark word的鎖升級(jí)流程:
無(wú)鎖/可偏向
鎖對(duì)象剛創(chuàng)建時(shí)得院,沒(méi)有任何線程競(jìng)爭(zhēng)傻铣,對(duì)象處于無(wú)鎖狀態(tài)。在上面打印的空對(duì)象的內(nèi)存布局中祥绞,根據(jù)大小端非洲,最后3位001,表示處于無(wú)鎖態(tài)蜕径,并且處于不可偏向狀態(tài)两踏。這是因?yàn)樵趈dk中偏向鎖存在延遲4秒啟動(dòng),也就是說(shuō)在jvm啟動(dòng)后4秒后創(chuàng)建的對(duì)象才會(huì)開(kāi)啟偏向鎖兜喻,我們通過(guò)jvm參數(shù)-XX:BiasedLockingStartupDelay=0
取消這個(gè)延遲時(shí)間梦染,這時(shí)最后3位為101,表示當(dāng)前對(duì)象的鎖沒(méi)有被持有朴皆,并且處于可被偏向狀態(tài)帕识。
偏向鎖
在沒(méi)有線程競(jìng)爭(zhēng)的條件下,第一個(gè)獲取鎖的線程通過(guò)CAS將自己的threadId寫(xiě)入到該對(duì)象的mark word中遂铡,若后續(xù)該線程再次獲取鎖肮疗,需要比較當(dāng)前線程threadId和對(duì)象mark word中的threadId是否一致,如果一致那么可以直接獲取扒接,并且鎖對(duì)象始終保持對(duì)該線程的偏向伪货,也就是說(shuō)偏向鎖不會(huì)主動(dòng)釋放。
偏向鎖的CAS環(huán)節(jié)在修改thread id
無(wú)鎖和偏向鎖狀態(tài)時(shí)钾怔,偏向鎖標(biāo)記位是有值的碱呼,所以看后三位
輕量級(jí)鎖
當(dāng)兩個(gè)或以上線程交替獲取鎖,但并沒(méi)有在對(duì)象上并發(fā)的獲取鎖時(shí)宗侦,偏向鎖升級(jí)為輕量級(jí)鎖巍举。在此階段,線程采取CAS的自旋
方式嘗試獲取鎖凝垛,避免阻塞線程造成的cpu在用戶(hù)態(tài)和內(nèi)核態(tài)間轉(zhuǎn)換的消耗懊悯。
輕量級(jí)鎖的CAS環(huán)節(jié)在自旋獲取鎖
輕量級(jí)鎖和重量級(jí)鎖時(shí)蜓谋,已不需要偏向鎖標(biāo)記,所以只有后兩位
綜合上面3個(gè)階段炭分,整個(gè)加鎖狀態(tài)的變化流程如下:
- 主線程首先對(duì)user對(duì)象加鎖桃焕,首次加鎖為101偏向鎖
- 子線程等待主線程釋放鎖后,對(duì)user對(duì)象加鎖捧毛,這時(shí)將偏向鎖升級(jí)為00輕量級(jí)鎖
- 輕量級(jí)鎖解鎖后观堂,user對(duì)象無(wú)線程競(jìng)爭(zhēng),恢復(fù)為001無(wú)鎖態(tài)呀忧,并且處于不可偏向狀態(tài)师痕。如果之后有線程再?lài)L試獲取user對(duì)象的鎖,會(huì)直接加輕量級(jí)鎖而账,而不是偏向鎖
重量級(jí)鎖
當(dāng)兩個(gè)或以上線程并發(fā)的在同一個(gè)對(duì)象上進(jìn)行同步時(shí)胰坟,為了避免無(wú)用自旋消耗cpu,輕量級(jí)鎖會(huì)升級(jí)成重量級(jí)鎖泞辐。這時(shí)mark word中的指針指向的是monitor對(duì)象(也被稱(chēng)為管程或監(jiān)視器鎖)的起始地址笔横。在兩個(gè)線程同時(shí)競(jìng)爭(zhēng)user對(duì)象的鎖時(shí),會(huì)升級(jí)為10重量級(jí)鎖咐吼。
mark word中的其他信息
hashcode
無(wú)鎖態(tài)下的hashcode采用了延遲加載技術(shù)吹缔,在第一次調(diào)用hashCode()方法時(shí)才會(huì)計(jì)算寫(xiě)入。
只有在調(diào)用沒(méi)有被重寫(xiě)的Object.hashCode()方法或System.identityHashCode(Object)方法才會(huì)寫(xiě)入mark word锯茄,執(zhí)行用戶(hù)自定義的hashCode()方法不會(huì)被寫(xiě)入厢塘。
當(dāng)對(duì)象被加鎖后,mark word中就沒(méi)有足夠空間來(lái)保存hashCode了肌幽,這時(shí)hashcode會(huì)被移動(dòng)到重量級(jí)鎖的Object Monitor中晚碾。
epoch
偏向鎖的時(shí)間戳
分代年齡(age)
在jvm的垃圾回收過(guò)程中,每當(dāng)對(duì)象經(jīng)過(guò)一次Young GC牍颈,年齡都會(huì)加1,這里4位來(lái)表示分代年齡最大值為15琅关,這也就是為什么對(duì)象的年齡超過(guò)15后會(huì)被移到老年代的原因煮岁。在啟動(dòng)時(shí)可以通過(guò)添加參數(shù)來(lái)改變年齡閾值-XX:MaxTenuringThreshold
,當(dāng)設(shè)置的閾值超過(guò)15時(shí)涣易,啟動(dòng)時(shí)會(huì)報(bào)錯(cuò)画机。
Klass Pointer 類(lèi)型指針
Klass Pointer是一個(gè)指向方法區(qū)中Class信息的指針,虛擬機(jī)通過(guò)這個(gè)指針確定該對(duì)象屬于哪個(gè)類(lèi)的實(shí)例新症。在64位的JVM中步氏,支持指針壓縮功能,根據(jù)是否開(kāi)啟指針壓縮徒爹,Klass Pointer占用的大小將會(huì)不同:
- 未開(kāi)啟指針壓縮時(shí)荚醒,類(lèi)型指針占用8B (64bit)
- 開(kāi)啟指針壓縮情況下芋类,類(lèi)型指針占用4B (32bit)
在jdk6之后的版本中,指針壓縮是被默認(rèn)開(kāi)啟的
實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)存儲(chǔ)的是對(duì)象的真正有效數(shù)據(jù)界阁,也就是各個(gè)屬性字段的值侯繁,如果在擁有父類(lèi)的情況下,還會(huì)包含父類(lèi)的字段泡躯。字段的存儲(chǔ)順序會(huì)受到數(shù)據(jù)類(lèi)型長(zhǎng)度贮竟、以及虛擬機(jī)的分配策略的影響
對(duì)齊填充字節(jié)
在java對(duì)象中,需要對(duì)齊填充字節(jié)的原因是较剃,64位的jvm中對(duì)象的大小被要求向8字節(jié)對(duì)齊咕别,因此當(dāng)對(duì)象的長(zhǎng)度不足8字節(jié)的整數(shù)倍時(shí),需要在對(duì)象中進(jìn)行填充操作写穴。注意圖中對(duì)齊填充部分使用了虛線惰拱,這是因?yàn)樘畛渥止?jié)并不是固定存在的部分,這點(diǎn)在后面計(jì)算對(duì)象大小時(shí)具體進(jìn)行說(shuō)明