一庇楞、對(duì)象的創(chuàng)建過程
當(dāng)虛擬機(jī)遇到一條new 指令時(shí):
- 檢查
- 首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符合引用代表的類是否已被加載极祸、解析和初始化過慈格。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程
- 分配內(nèi)存
- 在類加載檢查通過后遥金,接下來虛擬機(jī)將為新生對(duì)象分配內(nèi)存浴捆。對(duì)象所需內(nèi)存的大小在類加載完成后便可確定,為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java 堆中劃分出來稿械。
指針碰撞: 假如Java 堆中內(nèi)存是絕對(duì)規(guī)整的选泻,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為“指針碰撞”
-
空閑列表:如果Java 堆中的內(nèi)存并不是規(guī)整的美莫,虛擬機(jī)就必須維護(hù)一個(gè)列表页眯,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象的實(shí)例茂嗓,并更新列表上的記錄餐茵,這種分配方式被稱為“空閑列表”
選擇哪種分配方式由Java 堆是否規(guī)整決定,而Java 堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定述吸。
- 并發(fā)情況下線程安全:
- 虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性
- 把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間這中進(jìn)行忿族,即每個(gè)線程在Java 堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)蝌矛。哪個(gè)線程分配內(nèi)存道批,就在哪個(gè)線程的TLAB 上分配,只有TLAB用完并分配新的TLAB時(shí)入撒,才需要同步鎖定隆豹。虛擬機(jī)是否使用TLAB,可以通過-XX:+/-UseTLAB 參數(shù)設(shè)定
- 初始化零值
- 虛擬機(jī)需要將分配到的內(nèi)存空間都初始化零值(不包括對(duì)象頭)茅逮,如果使用TLAB璃赡,這一工作過程也可以提前至TLAB 分配時(shí)進(jìn)行判哥。這一步操作保證了對(duì)象的實(shí)例字段在Java 代碼中可以不賦初值諒直接使用,程序能訪問到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值碉考。
- 設(shè)置對(duì)象頭
- 虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置塌计,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息侯谁、對(duì)象的哈希碼锌仅、對(duì)象的 GC 分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭(Object Header)這中墙贱。根據(jù)虛擬機(jī)當(dāng)前的運(yùn)行狀態(tài)的不同热芹,如是否啟用偏向鎖等,對(duì)象頭會(huì)有不同的設(shè)置方式惨撇。
- 執(zhí)行init 方法伊脓。
- 從虛擬機(jī)角度來看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了串纺,但從 Java 程序的視角來看丽旅,對(duì)象創(chuàng)建才剛剛開始——<init>方法還沒有執(zhí)行纺棺,所有的字段都還為零。所以邪狞,一般來說(由字節(jié)碼中是否跟隨 invokespecial 指令所決定)祷蝌,執(zhí)行 new 指令之后會(huì)接著執(zhí)行<init>方法,把對(duì)象按照程序員的意愿進(jìn)行初始化帆卓,這樣一個(gè)真正可用的對(duì)象才算安全產(chǎn)生出來巨朦。
二、對(duì)象的內(nèi)存布局
? ? 在 HotSpot 虛擬機(jī)中剑令,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為 3 塊區(qū)域:對(duì)象頭(Header)糊啡、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。
- 對(duì)象頭(Header)
? ? HotSpot 虛擬機(jī)的對(duì)象頭包括兩部分信息:
- 第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)
? ? 存儲(chǔ)如哈希碼(HashCode)吁津、GC 分代年齡棚蓄、鎖狀態(tài)標(biāo)志、線程持有的鎖碍脏、偏向線程ID梭依、偏向時(shí)間戳等,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(未開啟壓縮指針)中分別為 32bit 和 64bit典尾,官方稱為“Mark Word”.
- 另外一部分是類型指針
??類型指針即對(duì)象指向它的類元數(shù)據(jù)的指針役拴,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
- 實(shí)例數(shù)據(jù)(Instance Data)
??實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息钾埂,也是在程序代碼中所定義的各種類型的字段內(nèi)容河闰。無論是從父類繼承下來的科平,還是子類中定義的,都需要記錄起來姜性。這部分的存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略參數(shù)(FieldsAllocationStyle)的字段在 Java 源碼中定義順序的影響瞪慧。HotSpot 虛擬機(jī)默認(rèn)的分配策略為 longs/doubles、ints污抬、shorts/chars汞贸、bytes/booleans、oops(Ordinary Object Pointers)印机,從分配策略中可以看出矢腻,相同寬度的字段總是被分配到一起。在滿足這個(gè)前提條件的情況下射赛,在父類中定義的變量會(huì)出現(xiàn)在子類之前多柑。如果 CompactFields 參數(shù)值為true(默認(rèn)為true),那么子類之中較窄的變量也可能會(huì)插入到父類變量的空隙之中楣责。
- 對(duì)齊填充(Padding)
??對(duì)齊填充并不是必然存在的竣灌,也沒有特別的含義,它僅僅起著占位符的作用由于 HotSpot VM 的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是 8 字節(jié)的整數(shù)倍秆麸。當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒有對(duì)齊時(shí)初嘹,就需要通過對(duì)齊填充來補(bǔ)全。
三沮趣、對(duì)象的訪問定位
??Java 程序需要通過棧上的 reference 數(shù)據(jù)來操作堆上的具體對(duì)象屯烦。目前主流的訪問方式有使用句柄和直接指針兩種。
-
如果使用句柄訪問的話房铭,那么 Java 堆中將會(huì)劃出一塊內(nèi)存來作為句柄池,reference 中存儲(chǔ)的就是對(duì)象的句柄地址驻龟,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息。
- 優(yōu)勢(shì):reference 中存儲(chǔ)的是穩(wěn)定的句柄地址缸匪,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改句柄中的實(shí)例數(shù)據(jù)指針翁狐,而 reference 本身不需要修改。
-
如果使用直接指針訪問凌蔬,那么 Java 堆對(duì)象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息露懒,而 reference 中存儲(chǔ)的直接就是對(duì)象地址。
- 優(yōu)勢(shì):直接指針訪問方式的最大好處就是速度更快龟梦,它節(jié)省了一次指針定位的時(shí)間開銷隐锭,由于對(duì)象的訪問在 Java 中非常繁重,因此這類開銷積少成多后也是一項(xiàng)非臣品。可觀的執(zhí)行成來钦睡。
Sun HotSpot 是使用直接指針方式進(jìn)行對(duì)象訪問的。