我們在學習使用Java的過程中办陷,一般認為new出來的對象都是被分配在堆上,但是這個結(jié)論不是那么的絕對饵蒂,通過對Java對象分配的過程分析慰于,可以知道有兩個地方會導致Java中new出來的對象并一定分別在所認為的堆上。這兩個點分別是Java中的逃逸分析和TLAB(Thread Local Allocation Buffer)何暮。本文首先對這兩者進行介紹奄喂,而后對Java對象分配過程進行介紹。
1. 逃逸分析
1.1 逃逸分析的定義
逃逸分析海洼,是一種可以有效減少Java 程序中同步負載和內(nèi)存堆分配壓力的跨函數(shù)全局數(shù)據(jù)流分析算法跨新。通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上贰军。
在計算機語言編譯器優(yōu)化原理中玻蝌,逃逸分析是指分析指針動態(tài)范圍的方法,它同編譯器優(yōu)化原理的指針分析和外形分析相關(guān)聯(lián)词疼。當變量(或者對象)在方法中分配后俯树,其指針有可能被返回或者被全局引用,這樣就會被其他過程或者線程所引用贰盗,這種現(xiàn)象稱作指針(或者引用)的逃逸(Escape)许饿。
Java在Java SE 6u23以及以后的版本中支持并默認開啟了逃逸分析的選項。Java的 HotSpot JIT編譯器舵盈,能夠在方法重載或者動態(tài)加載代碼的時候?qū)Υa進行逃逸分析陋率,同時Java對象在堆上分配和內(nèi)置線程的特點使得逃逸分析成Java的重要功能。
1.2 逃逸分析的方法
Java Hotspot編譯器使用的是
[plain] view plain copy
Choi J D, Gupta M, Serrano M, et al. Escape analysis for Java[J]. Acm Sigplan Notices, 1999, 34(10): 1-19.?
Jong-Deok Choi, Manish Gupta, Mauricio Seffano,Vugranam C. Sreedhar, Sam Midkiff等在論文《Escape Analysis for Java》中描述的算法進行逃逸分析的秽晚。該算法引入了連通圖瓦糟,用連通圖來構(gòu)建對象和對象引用之間的可達性關(guān)系,并在次基礎(chǔ)上赴蝇,提出一種組合數(shù)據(jù)流分析法菩浙。由于算法是上下文相關(guān)和流敏感的,并且模擬了對象任意層次的嵌套關(guān)系句伶,所以分析精度較高劲蜻,只是運行時間和內(nèi)存消耗相對較大。
絕大多數(shù)逃逸分析的實現(xiàn)都基于一個所謂“封閉世界(closed world)”的前提:所有可能被執(zhí)行的考余,方法在做逃逸分析前都已經(jīng)得知先嬉,并且,程序的實際運行不會改變它們之間的調(diào)用關(guān)系 楚堤。但當真實的 Java 程序運行時疫蔓,這樣的假設(shè)并不成立。Java 程序擁有的許多特性身冬,例如動態(tài)類加載鳄袍、調(diào)用本地函數(shù)以及反射程序調(diào)用等等,都將打破所謂“封閉世界”的約定吏恭。
不管是在“封閉世界”還是在“開放世界”拗小,逃逸分析,作為一種算法而非編程語言的存在樱哼,吸引了國內(nèi)外大量的學者對其進行研究哀九。在這里本文就不進行學術(shù)上了論述了,有需要的可以參見谷歌學術(shù)搜索:http://www.gfsoso.com/scholar?q=Escape%20Analysis搅幅。
1.3 逃逸分析后的處理
經(jīng)過逃逸分析之后阅束,可以得到三種對象的逃逸狀態(tài)。
GlobalEscape(全局逃逸)茄唐, 即一個對象的引用逃出了方法或者線程息裸。例如蝇更,一個對象的引用是復制給了一個類變量,或者存儲在在一個已經(jīng)逃逸的對象當中呼盆,或者這個對象的引用作為方法的返回值返回給了調(diào)用方法年扩。
ArgEscape(參數(shù)級逃逸),即在方法調(diào)用過程當中傳遞對象的應(yīng)用給一個方法访圃。這種狀態(tài)可以通過分析被調(diào)方法的二進制代碼確定厨幻。
NoEscape(沒有逃逸),一個可以進行標量替換的對象腿时】龃啵可以不將這種對象分配在傳統(tǒng)的堆上。
編譯器可以使用逃逸分析的結(jié)果批糟,對程序進行一下優(yōu)化格了。
堆分配對象變成棧分配對象。一個方法當中的對象徽鼎,對象的引用沒有發(fā)生逃逸笆搓,那么這個方法可能會被分配在棧內(nèi)存上而非常見的堆內(nèi)存上。
消除同步纬傲。線程同步的代價是相當高的满败,同步的后果是降低并發(fā)性和性能。逃逸分析可以判斷出某個對象是否始終只被一個線程訪問叹括,如果只被一個線程訪問算墨,那么對該對象的同步操作就可以轉(zhuǎn)化成沒有同步保護的操作,這樣就能大大提高并發(fā)程度和性能汁雷。
矢量替代净嘀。逃逸分析方法如果發(fā)現(xiàn)對象的內(nèi)存存儲結(jié)構(gòu)不需要連續(xù)進行的話,就可以將對象的部分甚至全部都保存在CPU寄存器內(nèi)侠讯,這樣能大大提高訪問速度挖藏。
下面,我們看一下逃逸分析的例子厢漩。
[java] view plain copy
class Main {?
? public static void main(String[] args) {?
? ? example();?
? }?
? public static void example() {?
? ? Foo foo = new Foo(); //alloc?
? ? Bar bar = new Bar(); //alloc?
? ? bar.setFoo(foo);?
? }?
}?
class Foo {}?
class Bar {?
? private Foo foo;?
? public void setFoo(Foo foo) {?
? ? this.foo = foo;?
? }?
}?
在這個例子當中膜眠,我們創(chuàng)建了兩個對象,F(xiàn)oo對象和Bar對象溜嗜,同時我們把Foo對象的應(yīng)用賦值給了Bar對象的方法宵膨。此時,如果Bar對在堆上就會引起Foo對象的逃逸炸宵,但是辟躏,在本例當中,編譯器通過逃逸分析土全,可以知道Bar對象沒有逃出example()方法捎琐,因此這也意味著Foo也沒有逃出example方法会涎。因此,編譯器可以將這兩個對象分配到棧上瑞凑。
1.4 編譯器經(jīng)過逃逸分析的效果
測試代碼:
[java] view plain copy
package com.yang.test2;?
/**
* Created by yangzl2008 on 2015/1/29.
*/?
class EscapeAnalysis {?
? ? private static class Foo {?
? ? ? ? private int x;?
? ? ? ? private static int counter;?
? ? ? ? public Foo() {?
? ? ? ? ? ? x = (++counter);?
? ? ? ? }?
? ? }?
? ? public static void main(String[] args) {?
? ? ? ? long start = System.nanoTime();?
? ? ? ? for (int i = 0; i < 1000 * 1000 * 10; ++i) {?
? ? ? ? ? ? Foo foo = new Foo();?
? ? ? ? }?
? ? ? ? long end = System.nanoTime();?
? ? ? ? System.out.println("Time cost is " + (end - start));?
? ? }?
}?
設(shè)置Idea JVM運行參數(shù):
未開啟逃逸分析設(shè)置為:
[plain] view plain copy
-server -verbose:gc?
開啟逃逸分析設(shè)置為:
[plain] view plain copy
-server -verbose:gc -XX:+DoEscapeAnalysis?
在未開啟逃逸分析的狀況下運行情況如下:
[plain] view plain copy
[GC 5376K->427K(63872K), 0.0006051 secs]?
[GC 5803K->427K(63872K), 0.0003928 secs]?
[GC 5803K->427K(63872K), 0.0003639 secs]?
[GC 5803K->427K(69248K), 0.0003770 secs]?
[GC 11179K->427K(69248K), 0.0003987 secs]?
[GC 11179K->427K(79552K), 0.0003817 secs]?
[GC 21931K->399K(79552K), 0.0004342 secs]?
[GC 21903K->399K(101120K), 0.0002175 secs]?
[GC 43343K->399K(101184K), 0.0001421 secs]?
Time cost is 58514571?
開啟逃逸分析的狀況下冕碟,運行情況如下:
[plain] view plain copy
Time cost is 10031306?
未開啟逃逸分析時虚倒,運行上訴代碼趟脂,JVM執(zhí)行了GC操作进副,而在開啟逃逸分析情況下脏毯,JVM并沒有執(zhí)行GC操作云挟。同時姑原,操作時間上岭妖,開啟逃逸分析的程序運行時間是未開啟逃逸分析時間的1/5徽曲。
2. TLAB
JVM在內(nèi)存新生代Eden Space中開辟了一小塊線程私有的區(qū)域零截,稱作TLAB(Thread-local allocation buffer)。默認設(shè)定為占用Eden Space的1%秃臣。在Java程序中很多對象都是小對象且用過即丟涧衙,它們不存在線程共享也適合被快速GC,所以對于小對象通常JVM會優(yōu)先分配在TLAB上奥此,并且TLAB上的分配由于是線程私有所以沒有鎖開銷弧哎。因此在實踐中分配多個小對象的效率通常比分配一個大對象的效率要高。
也就是說稚虎,Java中每個線程都會有自己的緩沖區(qū)稱作TLAB(Thread-local allocation buffer)撤嫩,每個TLAB都只有一個線程可以操作,TLAB結(jié)合bump-the-pointer技術(shù)可以實現(xiàn)快速的對象分配蠢终,而不需要任何的鎖進行同步序攘,也就是說,在對象分配的時候不用鎖住整個堆寻拂,而只需要在自己的緩沖區(qū)分配即可程奠。
關(guān)于對象分配的JDK源碼可以參見JVM 之 Java對象創(chuàng)建[初始化]中對OpenJDK源碼的分析。
3. Java對象分配的過程
編譯器通過逃逸分析祭钉,確定對象是在棧上分配還是在堆上分配瞄沙。如果是在堆上分配,則進入選項2.
如果tlab_top + size <= tlab_end慌核,則在在TLAB上直接分配對象并增加tlab_top 的值帕识,如果現(xiàn)有的TLAB不足以存放當前對象則3.
重新申請一個TLAB,并再次嘗試存放當前對象遂铡。如果放不下肮疗,則4.
在Eden區(qū)加鎖(這個區(qū)是多線程共享的),如果eden_top + size <= eden_end則將對象存放在Eden區(qū)扒接,增加eden_top 的值伪货,如果Eden區(qū)不足以存放们衙,則5.
執(zhí)行一次Young GC(minor collection)。
經(jīng)過Young GC之后碱呼,如果Eden區(qū)任然不足以存放當前對象蒙挑,則直接分配到老年代。
對象不在堆上分配主要的原因還是堆是共享的愚臀,在堆上分配有鎖的開銷忆蚀。無論是TLAB還是棧都是線程私有的,私有即避免了競爭(當然也可能產(chǎn)生額外的問題例如可見性問題)姑裂,這是典型的用空間換效率的做法馋袜。
TLAB的全稱是Thread Local Allocation Buffer,即線程本地分配緩存區(qū)舶斧,這是一個線程專用的內(nèi)存分配區(qū)域欣鳖。
由于對象一般會分配在堆上,而堆是全局共享的茴厉。因此在同一時間泽台,可能會有多個線程在堆上申請空間。因此矾缓,每次對象分配都必須要進行同步(虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性)怀酷,而在競爭激烈的場合分配的效率又會進一步下降。JVM使用TLAB來避免多線程沖突嗜闻,在給對象分配內(nèi)存時胰坟,每個線程使用自己的TLAB,這樣可以避免線程同步泞辐,提高了對象分配的效率笔横。
TLAB本身占用eEden區(qū)空間,在開啟TLAB的情況下咐吼,虛擬機會為每個Java線程分配一塊TLAB空間吹缔。參數(shù)-XX:+UseTLAB開啟TLAB,默認是開啟的锯茄。TLAB空間的內(nèi)存非常小厢塘,缺省情況下僅占有整個Eden空間的1%,當然可以通過選項-XX:TLABWasteTargetPercent設(shè)置TLAB空間所占用Eden空間的百分比大小肌幽。
由于TLAB空間一般不會很大晚碾,因此大對象無法在TLAB上進行分配,總是會直接分配在堆上喂急。TLAB空間由于比較小格嘁,因此很容易裝滿。比如廊移,一個100K的空間糕簿,已經(jīng)使用了80KB探入,當需要再分配一個30KB的對象時,肯定就無能為力了懂诗。這時虛擬機會有兩種選擇蜂嗽,第一,廢棄當前TLAB殃恒,這樣就會浪費20KB空間植旧;第二,將這30KB的對象直接分配在堆上离唐,保留當前的TLAB病附,這樣可以希望將來有小于20KB的對象分配請求可以直接使用這塊空間。實際上虛擬機內(nèi)部會維護一個叫作refill_waste的值侯繁,當請求對象大于refill_waste時胖喳,會選擇在堆中分配泡躯,若小于該值贮竟,則會廢棄當前TLAB,新建TLAB來分配對象较剃。這個閾值可以使用TLABRefillWasteFraction來調(diào)整咕别,它表示TLAB中允許產(chǎn)生這種浪費的比例。默認值為64写穴,即表示使用約為1/64的TLAB空間作為refill_waste惰拱。默認情況下,TLAB和refill_waste都會在運行時不斷調(diào)整的啊送,使系統(tǒng)的運行狀態(tài)達到最優(yōu)偿短。如果想要禁用自動調(diào)整TLAB的大小,可以使用-XX:-ResizeTLAB禁用ResizeTLAB馋没,并使用-XX:TLABSize手工指定一個TLAB的大小昔逗。
-XX:+PrintTLAB可以跟蹤TLAB的使用情況。一般不建議手工修改TLAB相關(guān)參數(shù)篷朵,推薦使用虛擬機默認行為勾怒。
對象內(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)存相互交錯鹦赎,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表误堡,記錄上哪些內(nèi)存塊是可用的古话,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄锁施,這種分配方式稱為“空閑列表”(Free List)陪踩。
總結(jié)
總體流程
對象分配流程
如果開啟棧上分配,JVM會先進行棧上分配悉抵,如果沒有開啟棧上分配或則不符合條件的則會進行TLAB分配肩狂,如果TLAB分配不成功,再嘗試在eden區(qū)分配姥饰,如果對象滿足了直接進入老年代的條件傻谁,那就直接分配在老年代。