[toc]
HotSpot中的對(duì)象
對(duì)象的創(chuàng)建
Java對(duì)象創(chuàng)建大致有如下四種方式:
- new關(guān)鍵字這應(yīng)該是我們最常見(jiàn)和最常用最簡(jiǎn)單的創(chuàng)建對(duì)象的方式。
-
使用
newInstance()方法,這里包括Class類(lèi)的newInstance()方法和Constructor類(lèi)的newInstance()方法(前者其實(shí)也是調(diào)用后者)。 -
使用
clone()方法要使用clone()方法我們必須實(shí)現(xiàn)Cloneable接口,用clone()方法創(chuàng)建對(duì)象并不會(huì)調(diào)用任何構(gòu)造函數(shù),即我們所說(shuō)的淺拷貝 -
反序列化要實(shí)現(xiàn)反序列化我們需要讓我們的類(lèi)實(shí)現(xiàn)
Serializable接口。當(dāng)我們徐麗華和反序列化一個(gè)對(duì)象,JVM會(huì)給我們創(chuàng)建一個(gè)單獨(dú)的對(duì)象,在反序列化時(shí),JVM創(chuàng)建對(duì)象并不會(huì)調(diào)用任何構(gòu)造方法,即我們所說(shuō)的的深拷貝
上面的四種創(chuàng)建對(duì)象的方法除了第一種使用的new指令之外,其他三種都是使用invokespecial(構(gòu)造函數(shù)直接調(diào)用)。這里我們只說(shuō)new創(chuàng)建對(duì)象的方式,現(xiàn)在看看當(dāng)虛擬機(jī)遇到new指令的時(shí)候如何創(chuàng)建對(duì)象的。
類(lèi)加載檢查
虛擬機(jī)遇到一條new指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類(lèi)的符號(hào)引用并檢查這個(gè)符號(hào)引用代碼的類(lèi)是否被加載、解析、初始化過(guò)的如果沒(méi)有,則必須先執(zhí)行相應(yīng)的類(lèi)加載過(guò)程,關(guān)于類(lèi)加載機(jī)制和類(lèi)加載器詳細(xì)內(nèi)容之后介紹。
分配內(nèi)存
在類(lèi)的加載檢查通過(guò)后,虛擬機(jī)就將為新生對(duì)象分配內(nèi)存,對(duì)象所需內(nèi)存的大小在類(lèi)加載器完成后便可完全確定,為對(duì)象分配空間的任務(wù)具體便等同于從java堆中劃出一塊大小確定的內(nèi)存空間可以分為如下兩種情況討論:
- Java堆中內(nèi)存絕對(duì)規(guī)整所有用過(guò)的內(nèi)存都存放在一邊,空閑的你內(nèi)存被存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑的空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式成為指針碰撞(Bump The Pointer)。
- Java堆內(nèi)存不規(guī)整已被使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒(méi)辦法簡(jiǎn)單的進(jìn)行指針碰撞Lee,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄哪些內(nèi)存塊可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式為空間列表(Free List)
選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又右所采用的垃圾收集器是否代用壓縮整理功能決定。因此在使用Serial、ParNew等帶Compact過(guò)程的收集器時(shí),系統(tǒng)采用年的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法收集器時(shí)(說(shuō)明一下CMS收集器可以通過(guò)UseCMSCompactAtFullCollection或CMSFullGCsBeforeCompaction來(lái)整理內(nèi)存的),就通常采用空閑列表。
除如何劃分可用空間之外,另外一個(gè)需要考慮的問(wèn)題是對(duì)象創(chuàng)在虛擬機(jī)中非常頻繁的行為,即使僅僅修改一個(gè)指針指向的位置,在并發(fā)情況下也并非線(xiàn)程安全的,可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒(méi)來(lái)得及修改,對(duì)象B又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存。解決這個(gè)問(wèn)題有如下兩種方案:
- 對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步實(shí)際上虛擬機(jī)是采用CAS配上失敗重試的方式保證更新操作的原子性。
-
把內(nèi)存分配的動(dòng)作按照線(xiàn)程劃分在不同的空間之中進(jìn)行即每一個(gè)線(xiàn)程在Java堆中預(yù)先分配一小塊內(nèi)存,稱(chēng)為本地線(xiàn)程分配緩沖(TLAB,Thread Local Allocation Buffer),哪個(gè)線(xiàn)程要分配內(nèi)存,就在哪個(gè)線(xiàn)程的TLAB上分配,只有TLAB用完,分配新的TLAB時(shí)才需要同步鎖定,虛擬機(jī)是否使用TLAB,可以通過(guò)
-XX:+/-UseTLAB參數(shù)來(lái)設(shè)定。
初始化
內(nèi)存分配完成后,虛擬機(jī)要設(shè)置對(duì)象的信息(如這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例、如何才能找到類(lèi)的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息)并存放在對(duì)象的對(duì)象頭(Object Header)中。根據(jù)虛擬機(jī)當(dāng)前的運(yùn)行狀態(tài)的不同,如是否啟用偏向鎖等,對(duì)象頭會(huì)有不同的設(shè)置方式。
執(zhí)行init方法
在完成上面工作之后,在虛擬機(jī)的視角來(lái)看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了,但是在Java程序的視角來(lái)看,對(duì)象創(chuàng)建材剛剛開(kāi)始----init方法還沒(méi)有執(zhí)行,所有的字段都還為零值。所以一般來(lái)說(shuō)(由字節(jié)碼中是否跟隨有invokespecial指令所決定),new指令之后會(huì)接著執(zhí)行init方法,把對(duì)象按照程序要的意愿進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全產(chǎn)生出來(lái)。
對(duì)象的內(nèi)存布局
HotSpot虛擬機(jī)中,對(duì)象內(nèi)存中存儲(chǔ)的補(bǔ)助可以分為三塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)
對(duì)象頭
HotSpot虛擬機(jī)的對(duì)象頭包括兩部分信息:
- 對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)Mark Word如哈希嗎(HashCode)、GC分代年齡。鎖狀態(tài)標(biāo)志、線(xiàn)程持有的鎖、偏向線(xiàn)程ID、偏向時(shí)間戳等等,這部分?jǐn)?shù)據(jù)的產(chǎn)能廣度在32位和64位的虛擬機(jī)(暫時(shí)不考慮開(kāi)啟壓縮指針的場(chǎng)景)中分別為32個(gè)和64個(gè)Bits,官方稱(chēng)為Mark Word。對(duì)象需要存儲(chǔ)的運(yùn)行數(shù)據(jù)很多,其實(shí)已經(jīng)超過(guò)了32、64為BitMap結(jié)構(gòu)所能記錄的限度,但是對(duì)象頭信息是與對(duì)象自身定義的數(shù)據(jù)無(wú)關(guān)的額外存儲(chǔ)成本,考慮到虛擬機(jī)的空間小了,Mark Word被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息,他會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間。例如在32位的HotSpot虛擬機(jī)中對(duì)象未被鎖定的狀態(tài)下,Mark Word的32個(gè)Bits空間中的25Bits用于存儲(chǔ)對(duì)象哈希嗎(HashCode)、4Bits用于存儲(chǔ)對(duì)象分代年齡,2Bits用于存儲(chǔ)鎖標(biāo)志位,1Bits固定為0,在其他狀態(tài)(輕量級(jí)鎖定、重量級(jí)鎖定、GC標(biāo)志、可偏向)下對(duì)象存儲(chǔ)內(nèi)容如下圖
| 存儲(chǔ)內(nèi)容 | 標(biāo)志位 | 狀態(tài) |
|---|---|---|
| 對(duì)象哈希碼、對(duì)象分代年齡 | 01 | 未鎖定 |
| 指向鎖記錄的指針 | 00 | 輕量級(jí)鎖定 |
| 指向重量級(jí)鎖定 | 10 | 膨脹(重量級(jí)鎖定) |
| 空,不需要記錄信息 | 11 | GC標(biāo)志 |
| 偏向線(xiàn)程ID、偏向時(shí)間戳、對(duì)象分代年齡 | 01 | 可偏向 |
- 類(lèi)型指針類(lèi)型指針即對(duì)象指向它的類(lèi)元數(shù)據(jù)指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例,并不是所有蘇你就實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上暴露類(lèi)型指針你,換句話(huà)說(shuō)查找對(duì)象的元數(shù)據(jù)信息并不一定要經(jīng)過(guò)對(duì)象本身,另外如果對(duì)象是一個(gè)Java數(shù)組,那在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過(guò)普通Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象大小,但是從數(shù)組的元數(shù)據(jù)中無(wú)法確定數(shù)組長(zhǎng)度。
實(shí)例數(shù)據(jù)
實(shí)例數(shù)據(jù)是對(duì)象真正存儲(chǔ)的有效信息,也即是我們?cè)诔绦虼a里面所定義的各種類(lèi)型的字段內(nèi)容,無(wú)論是從父類(lèi)繼承下來(lái)的,還是在子類(lèi)中定義的都需要記錄起來(lái)。這部分的存儲(chǔ)順序回收到虛擬機(jī)分配策略參數(shù)(FieldsAllocationStyle)和字段在Java源碼中定義順序的影響。HotSpot虛擬機(jī)默認(rèn)分配策略為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Oridinary Object Pointers),從分配策略中可以看出,相同寬度的字段總是被分配到一起。在滿(mǎn)足這個(gè)前提條件的情況下,在父類(lèi)中定義的變量會(huì)出現(xiàn)在子類(lèi)之前。如果CompactFields參數(shù)值為true(默認(rèn)是true),那么子類(lèi)中較窄的變量也可能會(huì)插入到父類(lèi)變量的空隙之中。
對(duì)齊填充
對(duì)齊填充并不是必然存在的,也沒(méi)用特別的含義,他僅僅起著占位符的作用,由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,換句話(huà)說(shuō)就是對(duì)象大小必須是8字節(jié)的正數(shù)被。對(duì)象頭部分好似8字節(jié)的半數(shù)(1倍或者2倍),因此當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊的話(huà),就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。
對(duì)象的訪(fǎng)問(wèn)
我們的Java程序需要通過(guò)棧上的對(duì)象引用(Rreferance)數(shù)據(jù)(存儲(chǔ)在棧上的局部變量表中)來(lái)操作堆上的具體對(duì)象由于reference類(lèi)型在Java虛擬機(jī)規(guī)范里面也只規(guī)定了一個(gè)指向?qū)ο笠?,并沒(méi)有定義這個(gè)引用的具體實(shí)現(xiàn),對(duì)象訪(fǎng)問(wèn)的方式也是取決于虛擬機(jī)實(shí)現(xiàn)而定的。主鏈訪(fǎng)問(wèn)方式由使用句柄和直接指針兩種。
使用句柄訪(fǎng)問(wèn)
如果使用句柄訪(fǎng)問(wèn)的話(huà),Java堆中將會(huì)劃分出一塊內(nèi)存作為存放句柄池,reference中存儲(chǔ)的就是對(duì)象句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類(lèi)型數(shù)據(jù)的各自的具體地址信息,如下圖所示:
使用指針直接訪(fǎng)問(wèn)
如果使用直接指針訪(fǎng)問(wèn)的話(huà),Java堆對(duì)象布局中就必須考慮如何讓制類(lèi)型數(shù)據(jù)的相關(guān)信息,reference中存儲(chǔ)的直接就是對(duì)象地址,如下圖所示:
這兩種對(duì)象訪(fǎng)問(wèn)方式各有優(yōu)勢(shì),下面分別談一下:
- 使用句柄訪(fǎng)問(wèn)的最大好處就是reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要被修改。
- 使用直接指針來(lái)訪(fǎng)問(wèn)最大的好處就是速度快,他節(jié)省了一次指針定位的時(shí)間開(kāi)銷(xiāo),由于對(duì)象訪(fǎng)問(wèn)在Java中非常頻繁,因此這類(lèi)開(kāi)銷(xiāo)及積小成多也是一項(xiàng)非??捎^的執(zhí)行成本。HotSpot是使用直接指針進(jìn)行對(duì)象訪(fǎng)問(wèn)的。