Java-對(duì)象的創(chuàng)建和分配

一、虛擬機(jī)中對(duì)象的創(chuàng)建過(guò)程

虛擬機(jī)中對(duì)象的創(chuàng)建過(guò)程.png

在這個(gè)圖中,藍(lán)色部分是在JVM遇到一個(gè)字節(jié)碼new指令時(shí)進(jìn)行的。
類(lèi)加載步驟,就是將對(duì)應(yīng)的.class文件加載到內(nèi)存中。
而分配內(nèi)存的步驟,包含了為對(duì)象分配內(nèi)存和并發(fā)安全問(wèn)題。

而對(duì)象的創(chuàng)建過(guò)程,一般有以下六個(gè)步驟:

(1)判斷對(duì)象對(duì)應(yīng)的類(lèi)是否加載、連接和初始化
首先會(huì)去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類(lèi)的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類(lèi)是否已經(jīng)被類(lèi)加載器加載、鏈接和初始化

(2)為對(duì)象分配內(nèi)存
類(lèi)加載完成后,會(huì)在java堆中劃分一塊內(nèi)存分配給對(duì)象。內(nèi)存分配根據(jù)java堆是否規(guī)整,有兩種方法:
(1)指針碰撞:即在分配內(nèi)存時(shí)將位于中間的指針指示器向空閑的內(nèi)存移動(dòng)一段與對(duì)象大小相等的距離。指針碰撞只能在內(nèi)存空間規(guī)整的使用
(2)空閑列表:不規(guī)整,則由虛擬機(jī)維護(hù)一個(gè)空閑列表來(lái)記錄可用內(nèi)存,然后查找足夠大的內(nèi)存分配給對(duì)象,更新空閑列表
使用什么方式進(jìn)行分配內(nèi)存,是由內(nèi)存的規(guī)整度決定的,而內(nèi)存的規(guī)整度是由垃圾回收器決定的,是否帶整理。比如,帶整理的,可以使用指針碰撞的方式,這樣的方式速度比較快,不需要另外維護(hù)一張表。

在JVM中,一個(gè)對(duì)象占據(jù)的內(nèi)存一定是連續(xù)的。

(3)處理并發(fā)安全問(wèn)題
有兩種方式:
(1)對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行通不處理,不如在虛擬機(jī)采用CAS算法并配上失敗重試的方式保證更新操作的原子性
(2)每個(gè)線程在java堆中預(yù)先分配一小塊內(nèi)存(一般在eden區(qū)),這塊內(nèi)存稱(chēng)為本地線程分配緩存,線程需要分配內(nèi)存時(shí),就在對(duì)應(yīng)線程的TLAB(本地線程分配緩沖)上分配內(nèi)存,當(dāng)線程中的TLAB用完并且被分配到了新的TLAB時(shí),這時(shí)候才需要同步鎖定。通過(guò)-XX:+/-UserTLAB參數(shù)來(lái)設(shè)置虛擬機(jī)是否使用TLAB。就是實(shí)現(xiàn)劃分好一塊內(nèi)存區(qū)域給對(duì)象,一般是一小塊內(nèi)存占百分之一,如果這塊內(nèi)存太小,則從新劃一塊更大的給對(duì)應(yīng)的對(duì)象。默認(rèn)情況下TLAB是開(kāi)啟的

(4)初始化分配到的內(nèi)存空間
除了對(duì)象頭以外的都初始化為零值,不是構(gòu)造方法。int類(lèi)型的就是0,boolean就是false

(5)設(shè)置對(duì)象的對(duì)象頭
將對(duì)象的所屬類(lèi)、對(duì)象的HashCode和對(duì)象的GC分代年齡等數(shù)據(jù)存儲(chǔ)在對(duì)象的對(duì)象頭中。

(6)執(zhí)行init方法進(jìn)行初始化
初始化對(duì)象的成員變量、調(diào)用類(lèi)的構(gòu)造方法,這樣一個(gè)對(duì)象就被創(chuàng)建出來(lái)。調(diào)用構(gòu)造方法init。
這里的初始化與類(lèi)加載過(guò)程中的第五步初始化是有點(diǎn)區(qū)別的,類(lèi)加載過(guò)程中的第五步的初始化,是初始化比如靜態(tài)代碼塊、靜態(tài)屬性和其他屬性的賦值動(dòng)作等等,會(huì)放在一個(gè)<client>方法中,這個(gè)方法是在<init>這個(gè)構(gòu)造器之前執(zhí)行的。

二、對(duì)象的內(nèi)存布局

一個(gè)對(duì)象一般在內(nèi)存中分為三塊:對(duì)象頭、實(shí)例數(shù)據(jù)、對(duì)齊填充(非必須)

1.對(duì)象頭

對(duì)象頭分為三個(gè)部分:Mark World,類(lèi)型指針,array length(用于記錄數(shù)組長(zhǎng)度,非數(shù)組則沒(méi)有這部分內(nèi)容)

(1)Mark World模型
image.png

這里對(duì)對(duì)象頭的長(zhǎng)度進(jìn)行說(shuō)明:如果是32位的情況下,那么對(duì)象頭的長(zhǎng)度一般是兩字寬,一字寬是4字節(jié)32位,兩字寬就是64位,這里的兩字寬組成,其實(shí)是Mark World長(zhǎng)度為一字寬,類(lèi)型指針的長(zhǎng)度為一字寬,如果是數(shù)組的話,其對(duì)象頭的長(zhǎng)度則還需要再加一字寬,即對(duì)象頭變成三字寬12字節(jié)96位;如果是64位的虛擬機(jī),則Mark World的長(zhǎng)度就是兩字寬8字節(jié)64位,那么對(duì)象頭的最大長(zhǎng)度就是192位。

(2)類(lèi)型指針

即指向該對(duì)象所屬類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)通常通過(guò)這個(gè)指針來(lái)確定該對(duì)象所屬的類(lèi)型

(3)array length

如果對(duì)象是一個(gè)數(shù)組,在對(duì)象頭中還應(yīng)該有一個(gè)記錄數(shù)組長(zhǎng)度的數(shù)據(jù),因?yàn)镴VM可以通過(guò)對(duì)象的元數(shù)據(jù)確定對(duì)象的大小,但是不能通過(guò)對(duì)象的元數(shù)據(jù)確定對(duì)象的長(zhǎng)度

2.實(shí)例數(shù)據(jù)

實(shí)例數(shù)據(jù)存儲(chǔ)的是真正的有效數(shù)據(jù),即各個(gè)字段的值。無(wú)論是子類(lèi)中定義的,還是從父類(lèi)中繼承下來(lái)的都需要記錄。這部分?jǐn)?shù)據(jù)的存儲(chǔ)順序受到虛擬機(jī)的分配策略以及字段在類(lèi)中定義順序的影響。

3.對(duì)齊補(bǔ)充

這部分?jǐn)?shù)據(jù)不是必然存在的,因?yàn)閷?duì)象的大小總是8字節(jié)的整數(shù)倍,該數(shù)據(jù)僅用于補(bǔ)齊實(shí)例數(shù)據(jù)部分不足整數(shù)倍的部分,充當(dāng)占位符的作用。
hotspot管理的對(duì)象,對(duì)對(duì)象的大小是有要求的,所以hotspot中對(duì)象對(duì)應(yīng)的大小必須是8字節(jié)的一個(gè)整數(shù)倍,如果實(shí)例數(shù)據(jù)剛好是8字節(jié)的整數(shù)倍,則不需要對(duì)齊補(bǔ)充。

三、對(duì)象的訪問(wèn)定位

1.通過(guò)句柄的方式訪問(wèn)

通過(guò)句柄的方式訪問(wèn).png

通過(guò)句柄訪問(wèn)的實(shí)現(xiàn)方式中,JVM堆中會(huì)專(zhuān)門(mén)有一塊區(qū)域用來(lái)作為句柄池,存儲(chǔ)相關(guān)句柄所執(zhí)行的實(shí)例數(shù)據(jù)地址(包括在堆中的地址和在方法區(qū)中的地址)。這種實(shí)現(xiàn)方式由于用句柄表示地址,因此十分穩(wěn)定。這樣的方式,需要 通過(guò)二次查找,會(huì)多一次指針定位的開(kāi)銷(xiāo),一次開(kāi)銷(xiāo)雖然小,但是JVM中會(huì)創(chuàng)建大量的對(duì)象,所以累積開(kāi)銷(xiāo)就比較大。但是這樣做的好處就是當(dāng)對(duì)象實(shí)例數(shù)據(jù)發(fā)生改變時(shí),并不需要改變棧中的指針指向,只需要改變句柄的指針指向即可。

2.直接訪問(wèn)方式訪問(wèn)

直接訪問(wèn)方式定位.png

通過(guò)直接訪問(wèn)的方式中,reference中存儲(chǔ)的就是對(duì)象在堆中的實(shí)際地址,在堆中存儲(chǔ)的對(duì)象信息包含了到方法區(qū)中的響應(yīng)的對(duì)象類(lèi)型數(shù)據(jù)的指針。這種方式的最大優(yōu)勢(shì)就是查詢(xún)速度快,在hotspot中使用的就是這種方式。
直接訪問(wèn)的方式,其實(shí)就是棧中有一個(gè)指針指向Java堆中的對(duì)象實(shí)例數(shù)據(jù),而Java堆中的對(duì)象實(shí)例數(shù)據(jù)中又有一個(gè)指針指向了方法區(qū)中的對(duì)象類(lèi)型數(shù)據(jù)。
現(xiàn)在我們一般理解的JVM堆棧其實(shí)就是這樣的。

對(duì)象類(lèi)型數(shù)據(jù),其實(shí)就是對(duì)象對(duì)應(yīng)的類(lèi)的Class信息。

四、對(duì)象的存活判斷

1.引用計(jì)數(shù)法

每個(gè)對(duì)象都有一個(gè)引用計(jì)數(shù)器,被引用則加1,引用失效則減1。當(dāng)引用計(jì)數(shù)器的值變?yōu)?時(shí),說(shuō)明該對(duì)象沒(méi)有被引用,被定義為垃圾對(duì)象。因?yàn)橐糜?jì)數(shù)法沒(méi)有解決對(duì)象之間相互循環(huán)引用的問(wèn)題,在這樣的情況下,會(huì)出現(xiàn)兩個(gè)對(duì)象相互引用,但是外部其他對(duì)象并沒(méi)有引用這兩個(gè)對(duì)象,會(huì)讓虛擬機(jī)認(rèn)為這兩個(gè)對(duì)象仍然是存活的,而不會(huì)被回收

2.可達(dá)性算法

可達(dá)性分析:如果一些對(duì)象通過(guò)等號(hào)等方式與GC的對(duì)象是可達(dá)的,那么這些對(duì)象就不會(huì)被回收。而如果與GC ROOTS對(duì)象是不可達(dá)的,那么就是可以被回收。
方法的基本思想是通過(guò)一系列的“GC Roots”對(duì)象作為起點(diǎn)進(jìn)行搜索,如果在“GC Roots”和一個(gè)對(duì)象之間沒(méi)有可達(dá)路徑,則稱(chēng)該對(duì)象是不可達(dá)的,不過(guò)要注意的是被判定為不可達(dá)的對(duì)象不一定就會(huì)成為可回收對(duì)象。被判定為不可達(dá)的對(duì)象要成為可回收對(duì)象必須至少經(jīng)歷兩次標(biāo)記過(guò)程,如果在這兩次標(biāo)記過(guò)程中仍然沒(méi)有逃脫成為可回收對(duì)象的可能性,則基本上就真的成為可回收對(duì)象了。最后面兩句將object1和object2賦值為null,也就是說(shuō)object1和object2指向的對(duì)象已經(jīng)不可能再被訪問(wèn),但是由于它們互相引用對(duì)方,導(dǎo)致它們的引用計(jì)數(shù)都不為0,那么垃圾收集器就永遠(yuǎn)不會(huì)回收它們。

可達(dá)性分析算法.png

可以作為GC Roots的對(duì)象一般包括以下幾種:

(1)虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象(線程棧變量即局部變量表中的變量)
(2)方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象(靜態(tài)變量)
(3)方法區(qū)中常量引用的對(duì)象(常量池:final常量)
(4)本地方法中JNI(一般說(shuō)的native方法)引用的對(duì)象(JNI指針)

五、對(duì)象分配策略

對(duì)象內(nèi)存分配.png

幾乎所有的對(duì)象都是在內(nèi)存中分配的,但是也有例外。有些對(duì)象也可以在棧上分配。虛擬機(jī)經(jīng)過(guò)逃逸分析之后,如果一個(gè)對(duì)象永遠(yuǎn)也不會(huì)發(fā)生逃逸,且不是大對(duì)象,那么該對(duì)象也是可以在棧上分配的,比如方法中創(chuàng)建的局部變量,該對(duì)象的定義和初始化都在方法內(nèi),并且該對(duì)象不會(huì)外部線程和其他線程都無(wú)法知道,所以是無(wú)法發(fā)生逃逸的,則可以分配在棧上。逃逸分析,一般就是不會(huì)方法逃逸,也不會(huì)線程逃逸,這樣的可以分配在棧上。
比如:

/**
 * 滿足逃逸分析,創(chuàng)建的MyObject對(duì)象并沒(méi)有方法逃逸,也沒(méi)有線程逃逸,所以在棧上分配對(duì)象
 * 這樣不會(huì)進(jìn)行垃圾回收,所以逃逸分析可以提高性能,減少GC
 */
public class EscapeAnalysisTest {

    public static void main(String[] args) {
        for (int i = 0; i<5000000;i++) {
            allocate();
        }
    }

    public static void allocate() {
        MyObject object = new MyObject();
    }
}

class MyObject{}
棧中分配(需要經(jīng)過(guò)逃逸分析之后)

在棧中分配對(duì)象,其實(shí)就是經(jīng)過(guò)逃逸分析之后,如果一個(gè)對(duì)象沒(méi)有發(fā)生逃逸,即在對(duì)象在子線程中被分配,并且指向該對(duì)象的指針永遠(yuǎn)不會(huì)發(fā)生逃逸(即只有創(chuàng)建線程可以看見(jiàn)該對(duì)象),那么經(jīng)過(guò)逃逸分析之后,可以將對(duì)象分配在棧中,而不是分配在堆中,減少了垃圾回收。
但是如果在方法中直接聲明了基本數(shù)據(jù)類(lèi)型的對(duì)象,那么就會(huì)在棧上直接分配。而其他引用對(duì)象在棧上分配,則需要經(jīng)過(guò)逃逸分析之后才可以。

1.對(duì)象分配策略

(1)優(yōu)先在Eden區(qū)分配
(2)空間分配擔(dān)保

垃圾回收回收新生代,采用minor GC,而回收老年代一般采用full GC。悲觀安全的做法:每一次晉級(jí)到老年代都進(jìn)程一次的major GC或者full GC。JVM提出了一個(gè)空間分配擔(dān)保,找JVM做擔(dān)保,如果擔(dān)保失敗,再進(jìn)行一次major或者full GC。
即:在GC之前,虛擬機(jī)會(huì)檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象總空間,如果小于則擔(dān)保失敗,如果大于則擔(dān)保成功,則會(huì)繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對(duì)象的平均大小,比如之前晉升了5次,平均大小為5M,則這個(gè)時(shí)候老年代的最大可用連續(xù)空間就需要大于5M,在小于的時(shí)候,就會(huì)通過(guò)full GC來(lái)讓老年代騰出更多的空間,如果大于的話,則通minor GC。通過(guò)老年代的擔(dān)保,是為了避免在大量對(duì)象存活的情況下,交換空間無(wú)法容納的對(duì)象可以直接進(jìn)入老年代。

(3)大對(duì)象直接進(jìn)入老年代

老年代與新生代的空間大小比例為2:1
為什么分成Eden、from、to,比例為8:1:1
大數(shù)據(jù)分析:90%的對(duì)象在第一次GC回收時(shí)就會(huì)被回收掉。而10%會(huì)進(jìn)入交換區(qū),所以一般來(lái)說(shuō),浪費(fèi)的空間其實(shí)就是10%,利用率有90%

(4)長(zhǎng)期存活的對(duì)象進(jìn)入老年代

Eden區(qū)的對(duì)象,其實(shí)是沒(méi)有年齡的,而From和To區(qū)中的對(duì)象才有年齡,而年齡則會(huì)保存在對(duì)象頭中的MarkWorld中。當(dāng)對(duì)象進(jìn)入老年代之后,也沒(méi)有年齡了。
長(zhǎng)期存活的對(duì)象,即GC之后年齡達(dá)到15的,就會(huì)進(jìn)入老年代。這里的年齡其實(shí)就是指的經(jīng)過(guò)GC的次數(shù),堅(jiān)持15次GC之后依然存活的默認(rèn)是長(zhǎng)期存活的。

(5)動(dòng)態(tài)對(duì)象年齡判定

From區(qū)和To區(qū)合稱(chēng)為survivor。如果交換空間中相同年齡的所有對(duì)象所占空間的大小總和大于交換空間的一半,則年齡大于或者等于該年齡的對(duì)象就可以直接進(jìn)入老年代。不需要等到年齡為15的時(shí)候

2.每個(gè)區(qū)域采用的算法

Eden區(qū)是采用標(biāo)記清除復(fù)制算法,復(fù)制是將存活對(duì)象從Eden區(qū)復(fù)制到Survivor的To空間。交換區(qū)也是采用標(biāo)記清除復(fù)制算法,而老年代一般也是采用標(biāo)記清除算法。除非是Full gc的時(shí)候,偶爾會(huì)整理。

逃逸分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容