Java中對象都是分配在堆上嗎?你錯了!
前言
我們在學(xué)習(xí)使用Java的過程中,一般認為new出來的對象都是被分配在堆上,但是這個結(jié)論不是那么的絕對,通過對Java對象分配的過程分析,可以知道有兩個地方會導(dǎo)致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)。當(dāng)變量(或者對象)在方法中分配后,其指針有可能被返回或者被全局引用,這樣就會被其他過程或者線程所引用,這種現(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)系 。但當(dāng)真實的 Java 程序運行時,這樣的假設(shè)并不成立。Java 程序擁有的許多特性,例如動態(tài)類加載、調(diào)用本地函數(shù)以及反射程序調(diào)用等等,都將打破所謂“封閉世界”的約定。
不管是在“封閉世界”還是在“開放世界”,逃逸分析,作為一種算法而非編程語言的存在,吸引了國內(nèi)外大量的學(xué)者對其進行研究。
1.3 逃逸分析后的處理
經(jīng)過逃逸分析之后,可以得到三種對象的逃逸狀態(tài)。
GlobalEscape(全局逃逸), 即一個對象的引用逃出了方法或者線程。例如,一個對象的引用是復(fù)制給了一個類變量,或者存儲在在一個已經(jīng)逃逸的對象當(dāng)中,或者這個對象的引用作為方法的返回值返回給了調(diào)用方法。
ArgEscape(參數(shù)級逃逸),即在方法調(diào)用過程當(dāng)中傳遞對象的應(yīng)用給一個方法。這種狀態(tài)可以通過分析被調(diào)方法的二進制代碼確定。
NoEscape(沒有逃逸),一個可以進行標(biāo)量替換的對象??梢圆粚⑦@種對象分配在傳統(tǒng)的堆上。
編譯器可以使用逃逸分析的結(jié)果,對程序進行一下優(yōu)化。
堆分配對象變成棧分配對象。一個方法當(dāng)中的對象,對象的引用沒有發(fā)生逃逸,那么這個方法可能會被分配在棧內(nèi)存上而非常見的堆內(nèi)存上。
消除同步。線程同步的代價是相當(dāng)高的,同步的后果是降低并發(fā)性和性能。逃逸分析可以判斷出某個對象是否始終只被一個線程訪問,如果只被一個線程訪問,那么對該對象的同步操作就可以轉(zhuǎn)化成沒有同步保護的操作,這樣就能大大提高并發(fā)程度和性能。
矢量替代。逃逸分析方法如果發(fā)現(xiàn)對象的內(nèi)存存儲結(jié)構(gòu)不需要連續(xù)進行的話,就可以將對象的部分甚至全部都保存在CPU寄存器內(nèi),這樣能大大提高訪問速度。
下面,我們看一下逃逸分析的例子。
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; } }
在這個例子當(dāng)中,我們創(chuàng)建了兩個對象,F(xiàn)oo對象和Bar對象,同時我們把Foo對象的應(yīng)用賦值給了Bar對象的方法。此時,如果Bar對在堆上就會引起Foo對象的逃逸,但是,在本例當(dāng)中,編譯器通過逃逸分析,可以知道Bar對象沒有逃出example()方法,因此這也意味著Foo也沒有逃出example方法。因此,編譯器可以將這兩個對象分配到棧上。
1.4 編譯器經(jīng)過逃逸分析的效果
測試代碼:
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è)置JVM運行參數(shù):
未開啟逃逸分析設(shè)置為:
-server -verbose:gc
開啟逃逸分析設(shè)置為:
-server -verbose:gc -XX:+DoEscapeAnalysis
在未開啟逃逸分析的狀況下運行情況如下:
[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
開啟逃逸分析的狀況下,運行情況如下:
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不足以存放當(dāng)前對象則3.
重新申請一個TLAB,并再次嘗試存放當(dāng)前對象。如果放不下,則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ū)任然不足以存放當(dāng)前對象,則直接分配到老年代。
對象不在堆上分配主要的原因還是堆是共享的,在堆上分配有鎖的開銷。無論是TLAB還是棧都是線程私有的,私有即避免了競爭(當(dāng)然也可能產(chǎn)生額外的問題例如可見性問題),這是典型的用空間換效率的做法。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Java畢業(yè)設(shè)計實戰(zhàn)之線上水果超市商城的實現(xiàn)
這是一個使用了java+SSM+springboot+redis開發(fā)的網(wǎng)上水果超市商城,是一個畢業(yè)設(shè)計的實戰(zhàn)練習(xí),具有水果超市商城該有的所有功能,感興趣的朋友快來看看吧2022-01-01淺析SpringBoot微服務(wù)中異步調(diào)用數(shù)據(jù)提交數(shù)據(jù)庫的問題
這篇文章主要介紹了SpringBoot微服務(wù)中異步調(diào)用數(shù)據(jù)提交數(shù)據(jù)庫的問題,今天本文涉及到的知識點不難,都是很簡單的crud操作,本文結(jié)合實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2022-07-07springcloud 中 zuul 修改請求參數(shù)信息的方法
這篇文章主要介紹了springcloud 中 zuul 修改請求參數(shù)信息的方法,需要的朋友可以參考下2018-02-02Spring Boot應(yīng)用配置常用相關(guān)視圖解析器詳解
這篇文章主要給大家介紹了關(guān)于Spring Boot應(yīng)用配置常用相關(guān)視圖解析器的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-12-12