虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)域描述了虛擬機(jī)管理的內(nèi)存劃分情況墙基,但是目前我們對(duì)于虛擬機(jī)還是有很多困惑,比如:
- 問題1:如何為對(duì)象分配內(nèi)存刷喜?
- 問題2:對(duì)象內(nèi)存模型是怎樣的残制?
- 問題3:是怎樣訪問內(nèi)存中的對(duì)象的?
- 問題4:分配內(nèi)存的時(shí)候如果遇到并發(fā)問題掖疮,怎么保證分配操作的線程安全性初茶?
為了搞清楚這些問題,我們先從虛擬機(jī)是如何創(chuàng)建對(duì)象開始講起浊闪。
一恼布、對(duì)象創(chuàng)建過程
當(dāng)虛擬機(jī)遇到一條new 指令時(shí),便會(huì)進(jìn)行對(duì)象的創(chuàng)建過程搁宾。
創(chuàng)建對(duì)象的過程如下:
- 檢查常量池中有沒有這個(gè)類的符號(hào)引用折汞,并且檢查這個(gè)符號(hào)引用代表的類有沒有被虛擬機(jī)加載過。
如果沒有被加載過盖腿,則執(zhí)行類加載過程爽待,然后進(jìn)入下一步;
如果已加載奸忽,則進(jìn)入下一步堕伪。
- 根據(jù)方法區(qū)中類的信息,在堆區(qū)劃分一塊確定大小的內(nèi)存給對(duì)象栗菜。
(經(jīng)過類加載后欠雌,類的信息被保存在方法區(qū)中,一個(gè)類的對(duì)象所需的內(nèi)存大小也固定下來疙筹。)
- 為對(duì)象的成員變量賦初始值
內(nèi)存分配完成之后富俄,需要對(duì)分配的內(nèi)存空間部分區(qū)域的內(nèi)容都初始化為零值。
這一步保證了對(duì)象成員變量在java代碼中可以不賦初始值而咆。
- 設(shè)置對(duì)象頭中的信息
關(guān)于對(duì)象頭是什么, 別急霍比,繼續(xù)往下看。 - 調(diào)用
<init>
方法進(jìn)行初始化
別再問<init>
是什么了,先往下看暴备。
二悠瞬、問題1解惑:
在堆區(qū)分配內(nèi)存有兩種方式。
- 指針碰撞法
如果堆中內(nèi)存是規(guī)整的,即所有用過的內(nèi)存都放在一邊浅妆,空閑的內(nèi)存放在另一邊望迎,中間用一個(gè)指針做分界點(diǎn)的指示器。
分配內(nèi)存的過程凌外,實(shí)際上就是指針向空閑空間那邊移動(dòng)一段與對(duì)象大小相等的距離辩尊。
- 空閑列表法
java堆中的內(nèi)存如果不是規(guī)整的,就需要使用空閑列表的分配方式康辑。
空閑列表概念:虛擬機(jī)維護(hù)了一個(gè)列表摄欲,用于記錄哪些內(nèi)存塊是可用的。
在分配的時(shí)候疮薇,從列表中找到一塊滿足對(duì)象大小的內(nèi)存空間劃分給對(duì)象實(shí)例胸墙,同時(shí)會(huì)更新列表上的記錄。
- 關(guān)于兩種分配方式的選擇
選擇哪種分配方式取決于java堆是否規(guī)整惦辛。
而java堆是否規(guī)整取決于所采用的垃圾收集器是否帶有壓縮整理的功能劳秋。
因此,選擇哪種分配方式最終取決于使用了哪種垃圾收集器胖齐。
- 使用了指針碰撞的垃圾收集器有哪些玻淑?
serial、ParNew等基于復(fù)制算法或標(biāo)記整理(Mark Compact)算法的收集器呀伙,不會(huì)導(dǎo)致內(nèi)存碎片补履,因此使用的是指針碰撞。
- 采用空閑鏈表垃圾收集器有哪些剿另?
CMS等基于Mark-Sweep(標(biāo)記清除)算法的收集器箫锤,會(huì)產(chǎn)生內(nèi)存碎片,所以使用空閑列表法雨女。
三谚攒、問題2解惑
對(duì)象在內(nèi)存中的數(shù)據(jù)除了實(shí)例本身的數(shù)據(jù)外,還包括對(duì)象頭和對(duì)齊填充
3.1 實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)存儲(chǔ)的是成員變量的值氛堕,包括從父類繼承下來的成員變量馏臭。
成員變量在內(nèi)存中的順序:相同寬度的字段會(huì)分配在一起,父類定義的變量會(huì)出現(xiàn)在子類之前讼稚,
默認(rèn)情況下括儒,子類中較窄的變量可能會(huì)被插入到父類變量的間隙中。反正就是不一定按定義的順序來分配锐想。
3.2 對(duì)象頭是什么帮寻?
對(duì)象頭的作用是記錄對(duì)象在運(yùn)行過程中所需的數(shù)據(jù)。
比如對(duì)象屬于哪個(gè)類的實(shí)例赠摇、所屬類的信息在方法區(qū)中的位置(類型指針)固逗、對(duì)象的哈希碼浅蚪、對(duì)象的GC分代年齡等信息。這些信息就保存在對(duì)象頭中(Object Header)
3.3 對(duì)齊填充又是什么抒蚜?
對(duì)齊填充是用于確保對(duì)象的內(nèi)存的總長(zhǎng)度為8字節(jié)的整數(shù)倍掘鄙。
為什么要是確保是8字節(jié)的整數(shù)倍呢?
因?yàn)閔otspot要求對(duì)象起始地址為8字節(jié)的整數(shù)倍以便于自動(dòng)內(nèi)存管理嗡髓,
換句話說,對(duì)象的總長(zhǎng)度要為8字節(jié)的整數(shù)倍才能保證如此收津。
而又因?yàn)閷?duì)象頭正好是8字節(jié)(32位或64位)的整數(shù)倍饿这,但是實(shí)例數(shù)據(jù)長(zhǎng)度是任意的,因此需要對(duì)齊補(bǔ)充來確保整個(gè)對(duì)象總長(zhǎng)度為8字節(jié)的整數(shù)倍撞秋。
四长捧、問題3解惑
java程序需要通過引用來操作堆上的具體數(shù)據(jù)。
根據(jù)引用存放的地址類型的不同吻贿,對(duì)象有不同的訪問方式
主要有兩種訪問方式:
- 使用句柄訪問
- 使用直接指針訪問
4.1 使用句柄訪問
堆中會(huì)劃分一塊內(nèi)存用來做句柄池串结。引用中存儲(chǔ)的就是對(duì)象的句柄地址。句柄包含了對(duì)象實(shí)例數(shù)據(jù)和對(duì)象類型的數(shù)據(jù)的指針舅列。
通過引用訪問對(duì)象的時(shí)候肌割,會(huì)首先根據(jù)引用找到對(duì)象的句柄,然后根據(jù)句柄中對(duì)象的地址來訪問對(duì)象帐要。
4.2 使用直接指針訪問
引用中存儲(chǔ)的直接是對(duì)象的地址把敞,直接通過引用來訪問對(duì)象。
4.3 兩種方式對(duì)比
-
使用句柄
- 優(yōu)點(diǎn):引用中存儲(chǔ)的是穩(wěn)定的句柄地址榨惠,發(fā)生垃圾收集時(shí)可能會(huì)移動(dòng)對(duì)象奋早,這時(shí)候只需要改變句柄中實(shí)例數(shù)據(jù)的指針指向新對(duì)象,而引用的值不需要改赠橙。
- 缺點(diǎn):需要兩次尋址耽装。
-
使用直接指針
- 優(yōu)點(diǎn):速度快,一次尋址即可期揪。
- 缺點(diǎn):需要在對(duì)象實(shí)例的內(nèi)存中保存一個(gè)指向方法區(qū)中該類型數(shù)據(jù)的指針掉奄。不過使用句柄方式句柄中也需要保存類型指針。
直接指針的速度快横侦,hotspot采用就是直接指針的方式
五挥萌、問題4解惑
對(duì)象分配內(nèi)存不是線程安全的,比如給對(duì)象A分配內(nèi)存枉侧,還沒來得及修改指針的指向引瀑,
另一個(gè)線程創(chuàng)建對(duì)象B也用了原來的指針,這樣就會(huì)出問題的榨馁。
如何解決憨栽?
- 方案1: 對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理
實(shí)際上虛擬機(jī)采用CAS配上失敗重試的方式保證更新指針操作的原子性。
- 方案2:把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間中進(jìn)行
即:每個(gè)線程在java堆中預(yù)分配一小塊內(nèi)存,
這一小塊內(nèi)存稱作“本地線程分配緩沖"(Thread Local Allocation Buffer, TLAB)
內(nèi)存分配的過程就可以總結(jié)為:不同線程使用指針碰撞或者空閑列表的方式在各自的TLAB上分配內(nèi)存屑柔。
當(dāng)線程的TLAB用完需要分配新的TLAB屡萤,這時(shí)候才需要同步內(nèi)存分配操作。
虛擬機(jī)是否需要使用TLAB掸宛,可以通過-XX:+/-UseTLAB參數(shù)來決定死陆。
六、遺留問題:<init>方法是個(gè)啥唧瘾?
從上面對(duì)象的創(chuàng)建過程措译,我們可以了解到,在內(nèi)存分配完成之后饰序,所有成員變量的值都還只是零值领虹。
對(duì)于虛擬機(jī)來說,對(duì)象創(chuàng)建已經(jīng)完畢求豫,但是塌衰,對(duì)于java程序來說,對(duì)象的初始化才剛開始蝠嘉。
成員變量的初始化工作交由<init>
方法的來完成最疆。
編譯器收集了成員變量上的賦值操作,實(shí)例初始化代碼塊的賦值操作是晨,以及構(gòu)造方法中的賦值操作肚菠,構(gòu)成了<init>
方法,并執(zhí)行罩缴,對(duì)象就得到了初始化蚊逢。
學(xué)習(xí)過java基礎(chǔ)的人都知道,初始化的順序?yàn)? 成員變量上的賦值-->實(shí)例初始化塊-->構(gòu)造方法箫章。
<init>
方法就解釋了為什么是這個(gè)過程烙荷。
七、講點(diǎn)對(duì)象頭
對(duì)象頭的內(nèi)存模型分三部分:
- Mark Word
- 類型指針
- 記錄數(shù)組的長(zhǎng)度
7.1 Mark Word
存放hashCode檬寂、GC分代年齡终抽、鎖狀態(tài)標(biāo)志、線程持有的鎖桶至、偏向線程id昼伴、偏向時(shí)間戳等。
長(zhǎng)度為32位或者64位(32位虛擬機(jī)和64位虛擬機(jī))镣屹。
mark word是一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)圃郊,在不同情況下結(jié)構(gòu)會(huì)有所變化。
比如:在32位的虛擬機(jī)中女蜈,如果對(duì)象處于未被鎖定的狀態(tài)持舆,
mark Word的32位空間將有25位用于存儲(chǔ)hashcode,
4位用于存儲(chǔ)對(duì)象的分代年齡色瘩,2位用于存儲(chǔ)對(duì)象上鎖
標(biāo)志,1位固定為0
這些東西我就不一個(gè)個(gè)介紹他們是用來干嘛的逸寓,講多了反而復(fù)雜居兆,大概了解就行,有興趣的可以百度竹伸。
7.2 類型指針
一個(gè)指向類元數(shù)據(jù)的指針泥栖,通過這個(gè)指針,可以確定對(duì)象是哪個(gè)類的實(shí)例佩伤。記住聊倔,這個(gè)指針是在對(duì)象頭中,但不是在Mark Word中的生巡。
7.3 數(shù)組長(zhǎng)度
如果對(duì)象是一個(gè)數(shù)組,在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)见妒。
這一部分僅在對(duì)象是數(shù)組的時(shí)候存在孤荣。
點(diǎn)贊是對(duì)我最大的鼓勵(lì)