在學(xué)習(xí)Java的過程中,很多喜歡說new出來的對象分配一定在對上玲躯;
其實不能這么說,只能說大部分對象分配是在對上鳄乏。通過對象的分配過
程分析跷车,除了堆以外,還有兩個地方可以存放對象:
棧和TLAB(Thread Local Allocation Buffer)橱野。
Java對象分配流程圖:
如果開啟棧上分配姓赤,JVM會先進(jìn)行棧上分配,如果沒有開啟棧上分配或則不符合條件的則會進(jìn)行TLAB分配仲吏,如果TLAB分配不成功不铆,再嘗試在eden區(qū)分配,如果對象滿足了直接進(jìn)入老年代的條件裹唆,那就直接分配在老年代誓斥。
棧上分配
在JVM中,堆是線程共享的许帐,因此堆上的對象對于各個線程都是共享和可見的劳坑,只要持有對象的引用,就可以訪問堆中存儲的對象數(shù)據(jù)成畦。虛擬機(jī)的垃圾收集系統(tǒng)可以回收堆中不再使用的對象距芬,但對于垃圾收集器來說,無論篩選可回收對象循帐,還是回收和整理內(nèi)存都需要耗費(fèi)時間框仔。
如果確定一個對象的作用域不會逃逸出方法之外,那可以將這個對象分配在棧上拄养,這樣离斩,對象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀。在一般應(yīng)用中,不會逃逸的局部對象所占的比例很大跛梗,如果能使用棧上分配寻馏,那大量的對象就會隨著方法的結(jié)束而自動銷毀了,無須通過垃圾收集器回收核偿,可以減小垃圾收集器的負(fù)載诚欠。
JVM允許將線程私有的對象打散分配在棧上,而不是分配在堆上漾岳。分配在棧上的好處是可以在函數(shù)調(diào)用結(jié)束后自行銷毀轰绵,而不需要垃圾回收器的介入,從而提高系統(tǒng)性能蝗羊。
棧上分配的技術(shù)基礎(chǔ):
一是逃逸分析:逃逸分析的目的是判斷對象的作用域是否有可能逃逸出函數(shù)體藏澳。關(guān)于逃逸分析的問題可以看我另一篇文章:
二是標(biāo)量替換:允許將對象打散分配在棧上,比如若一個對象擁有兩個字段耀找,會將這兩個字段視作局部變量進(jìn)行分配翔悠。
只能在server模式下才能啟用逃逸分析,參數(shù)-XX:DoEscapeAnalysis啟用逃逸分析野芒,參數(shù)-XX:+EliminateAllocations開啟標(biāo)量替換(默認(rèn)打開)蓄愁。Java SE 6u23版本之后,HotSpot中默認(rèn)就開啟了逃逸分析狞悲,可以通過選項-XX:+PrintEscapeAnalysis查看逃逸分析的篩選結(jié)果撮抓。
TLAB(Thread Local Allocation Buffer)
TLAB的全稱是Thread Local Allocation Buffer,即線程本地分配緩存區(qū)摇锋,這是一個線程專用的內(nèi)存分配區(qū)域丹拯。
由于對象一般會分配在堆上,而堆是全局共享的荸恕。因此在同一時間乖酬,可能會有多個線程在堆上申請空間。因此融求,每次對象分配都必須要進(jìn)行同步(虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性)咬像,而在競爭激烈的場合分配的效率又會進(jìn)一步下降。JVM使用TLAB來避免多線程沖突生宛,在給對象分配內(nèi)存時县昂,每個線程使用自己的TLAB,這樣可以避免線程同步陷舅,提高了對象分配的效率倒彰。
TLAB本身占用eEden區(qū)空間,在開啟TLAB的情況下蔑赘,虛擬機(jī)會為每個Java線程分配一塊TLAB空間狸驳。參數(shù)-XX:+UseTLAB開啟TLAB预明,默認(rèn)是開啟的缩赛。TLAB空間的內(nèi)存非常小耙箍,缺省情況下僅占有整個Eden空間的1%,當(dāng)然可以通過選項-XX:TLABWasteTargetPercent設(shè)置TLAB空間所占用Eden空間的百分比大小酥馍。
由于TLAB空間一般不會很大辩昆,因此大對象無法在TLAB上進(jìn)行分配,總是會直接分配在堆上旨袒。TLAB空間由于比較小汁针,因此很容易裝滿。比如砚尽,一個100K的空間施无,已經(jīng)使用了80KB,當(dāng)需要再分配一個30KB的對象時必孤,肯定就無能為力了猾骡。這時虛擬機(jī)會有兩種選擇,第一敷搪,廢棄當(dāng)前TLAB兴想,這樣就會浪費(fèi)20KB空間;第二赡勘,將這30KB的對象直接分配在堆上嫂便,保留當(dāng)前的TLAB,這樣可以希望將來有小于20KB的對象分配請求可以直接使用這塊空間闸与。實際上虛擬機(jī)內(nèi)部會維護(hù)一個叫作refill_waste的值毙替,當(dāng)請求對象大于refill_waste時,會選擇在堆中分配践樱,若小于該值厂画,則會廢棄當(dāng)前TLAB,新建TLAB來分配對象映胁。這個閾值可以使用TLABRefillWasteFraction來調(diào)整木羹,它表示TLAB中允許產(chǎn)生這種浪費(fèi)的比例。默認(rèn)值為64解孙,即表示使用約為1/64的TLAB空間作為refill_waste坑填。默認(rèn)情況下,TLAB和refill_waste都會在運(yùn)行時不斷調(diào)整的弛姜,使系統(tǒng)的運(yùn)行狀態(tài)達(dá)到最優(yōu)脐瑰。如果想要禁用自動調(diào)整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB廷臼,并使用-XX:TLABSize手工指定一個TLAB的大小苍在,-XX:+PrintTLAB可以跟蹤TLAB的使用情況绝页。一般不建議手工修改TLAB相關(guān)參數(shù),推薦使用虛擬機(jī)默認(rèn)行為寂恬。
所謂TLAB其實就是這樣的一個東西:(簡化偽代碼)
struct ThreadLocalAllocBuffer {
HeapWord* _start;
HeapWord* _top;
HeapWord* _end;
};
每個線程會從Eden分配一大塊空間续誉,例如說100KB,作為自己的TLAB初肉。這個start是TLAB的起始地址酷鸦,end是TLAB的末尾,然后top是當(dāng)前的分配指針牙咏。顯然start <= top < end臼隔。
在Eden分配空間時,用的是bump-the-pointer方式來分配妄壶,但由于Eden是所有Java線程所共享的摔握,在bump pointer的時候必須加鎖(或者CAS)才可以保證安全;而當(dāng)每個線程從Eden分配到一塊空間當(dāng)作TLAB來用之后丁寄,在TLAB里分配小塊空間同樣是bump-the-pointer(上面示意的top指針)則不需要加鎖氨淌。
當(dāng)一個Java線程在自己的TLAB中分配到盡頭之后,再要分配就會出發(fā)一次“TLAB refill”狡逢,也就是說之前自己的TLAB就“不管了”(所有權(quán)交回給共享的Eden)宁舰,然后重新從Eden里分配一塊空間作為新的TLAB。所謂“不管了”并不是說就讓舊TLAB里的對象直接死掉奢浑,而是把那塊空間的控制權(quán)歸還給普通的Eden蛮艰,里面的對象該怎樣還是怎樣。
通常情況下雀彼,在TLAB中分配多次才會填滿TLAB壤蚜、觸發(fā)TLAB refill,這樣使用TLAB分配就比直接從共享部分的Eden分配要均攤(amortized)了同步開銷徊哑,于是提高了性能袜刷。其實很多關(guān)注多線程性能的malloc庫實現(xiàn)也會使用類似的做法,例如TCMalloc莺丑。
到觸發(fā)GC的時候著蟹,無論是minor GC還是full GC,要收集Eden的時候里面的空間無論是屬于某個線程的TLAB還是不屬于任何TLAB都一視同仁梢莽,把Eden當(dāng)作一個整體來收集里面的對象——把活的對象拷貝到survivor space(或者直接晉升到Old Gen)萧豆。在GC結(jié)束之后,每個Java線程又會重新從Eden分配自己的TLAB昏名。周而復(fù)始涮雷。
想像這樣的代碼:
public class Test {
public static Test sharedStatic;
public Test sharedInstanceField;
public static void foo() {
Test localVar = new Test(); // 1
if (sharedStatic == null) {
sharedStatic = localVar; // 2
} else {
sharedStatic.sharedInstanceField = localVar; // 3
}
}
}
(這個例子純粹為了示意“獨(dú)占”與“共享”的概念,請不要吐槽線程安全問題 )轻局,我們在 (1) 創(chuàng)建了一個新的Test實例洪鸭。如題主所說样刷,在啟動UseTLAB(默認(rèn)開啟)的時候,這個Test實例會被分配在當(dāng)前執(zhí)行Test.foo()的線程的TLAB里览爵。TLAB在執(zhí)行分配動作的時候要更新top指針置鼻,而更新這個指針不需要加任何鎖。
對象內(nèi)存分配的兩種方法
為對象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來拾枣。
指針碰撞(Serial沃疮、ParNew等帶Compact過程的收集器)
假設(shè)Java堆中內(nèi)存是絕對規(guī)整的盒让,所有用過的內(nèi)存都放在一邊梅肤,空閑的內(nèi)存放在另一邊,中間放著一個指針作為分界點的指示器邑茄,那所分配內(nèi)存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離姨蝴,這種分配方式稱為“指針碰撞”(Bump the Pointer)。
空閑列表(CMS這種基于Mark-Sweep算法的收集器)
如果Java堆中的內(nèi)存并不是規(guī)整的肺缕,已使用的內(nèi)存和空閑的內(nèi)存相互交錯左医,那就沒有辦法簡單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個列表同木,記錄上哪些內(nèi)存塊是可用的浮梢,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄彤路,這種分配方式稱為“空閑列表”(Free List)秕硝。
參考:
https://www.zhihu.com/question/56538259
https://blog.csdn.net/yangsnow_rain_wind/article/details/80434323
https://blog.csdn.net/xiaomingdetianxia/article/details/77688945