溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Java中對(duì)象是不是都分配在堆上

發(fā)布時(shí)間:2021-09-06 14:23:27 來(lái)源:億速云 閱讀:93 作者:chen 欄目:編程語(yǔ)言

本篇內(nèi)容主要講解“Java中對(duì)象是不是都分配在堆上”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“Java中對(duì)象是不是都分配在堆上”吧!

前言

我們?cè)趯W(xué)習(xí)使用Java的過(guò)程中,一般認(rèn)為new出來(lái)的對(duì)象都是被分配在堆上,但是這個(gè)結(jié)論不是那么的絕對(duì),通過(guò)對(duì)Java對(duì)象分配的過(guò)程分析,可以知道有兩個(gè)地方會(huì)導(dǎo)致Java中new出來(lái)的對(duì)象并不一定分別在所認(rèn)為的堆上。這兩個(gè)點(diǎn)分別是Java中的逃逸分析和TLAB(Thread Local Allocation Buffer)。本文首先對(duì)這兩者進(jìn)行介紹,而后對(duì)Java對(duì)象分配過(guò)程進(jìn)行介紹。

1. 逃逸分析

1.1 逃逸分析的定義

逃逸分析,是一種可以有效減少Java 程序中同步負(fù)載和內(nèi)存堆分配壓力的跨函數(shù)全局?jǐn)?shù)據(jù)流分析算法。通過(guò)逃逸分析,Java Hotspot編譯器能夠分析出一個(gè)新的對(duì)象的引用的使用范圍從而決定是否要將這個(gè)對(duì)象分配到堆上。

在計(jì)算機(jī)語(yǔ)言編譯器優(yōu)化原理中,逃逸分析是指分析指針動(dòng)態(tài)范圍的方法,它同編譯器優(yōu)化原理的指針?lè)治龊屯庑畏治鱿嚓P(guān)聯(lián)。當(dāng)變量(或者對(duì)象)在方法中分配后,其指針有可能被返回或者被全局引用,這樣就會(huì)被其他過(guò)程或者線程所引用,這種現(xiàn)象稱(chēng)作指針(或者引用)的逃逸(Escape)。

Java在Java SE 6u23以及以后的版本中支持并默認(rèn)開(kāi)啟了逃逸分析的選項(xiàng)。Java的 HotSpot JIT編譯器,能夠在方法重載或者動(dòng)態(tài)加載代碼的時(shí)候?qū)Υa進(jìn)行逃逸分析,同時(shí)Java對(duì)象在堆上分配和內(nèi)置線程的特點(diǎn)使得逃逸分析成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》中描述的算法進(jìn)行逃逸分析的。該算法引入了連通圖,用連通圖來(lái)構(gòu)建對(duì)象和對(duì)象引用之間的可達(dá)性關(guān)系,并在次基礎(chǔ)上,提出一種組合數(shù)據(jù)流分析法。由于算法是上下文相關(guān)和流敏感的,并且模擬了對(duì)象任意層次的嵌套關(guān)系,所以分析精度較高,只是運(yùn)行時(shí)間和內(nèi)存消耗相對(duì)較大。

絕大多數(shù)逃逸分析的實(shí)現(xiàn)都基于一個(gè)所謂“封閉世界(closed world)”的前提:所有可能被執(zhí)行的,方法在做逃逸分析前都已經(jīng)得知,并且,程序的實(shí)際運(yùn)行不會(huì)改變它們之間的調(diào)用關(guān)系 。但當(dāng)真實(shí)的 Java 程序運(yùn)行時(shí),這樣的假設(shè)并不成立。Java 程序擁有的許多特性,例如動(dòng)態(tài)類(lèi)加載、調(diào)用本地函數(shù)以及反射程序調(diào)用等等,都將打破所謂“封閉世界”的約定。

不管是在“封閉世界”還是在“開(kāi)放世界”,逃逸分析,作為一種算法而非編程語(yǔ)言的存在,吸引了國(guó)內(nèi)外大量的學(xué)者對(duì)其進(jìn)行研究。

1.3 逃逸分析后的處理

經(jīng)過(guò)逃逸分析之后,可以得到三種對(duì)象的逃逸狀態(tài)。

GlobalEscape(全局逃逸), 即一個(gè)對(duì)象的引用逃出了方法或者線程。例如,一個(gè)對(duì)象的引用是復(fù)制給了一個(gè)類(lèi)變量,或者存儲(chǔ)在在一個(gè)已經(jīng)逃逸的對(duì)象當(dāng)中,或者這個(gè)對(duì)象的引用作為方法的返回值返回給了調(diào)用方法。

ArgEscape(參數(shù)級(jí)逃逸),即在方法調(diào)用過(guò)程當(dāng)中傳遞對(duì)象的應(yīng)用給一個(gè)方法。這種狀態(tài)可以通過(guò)分析被調(diào)方法的二進(jìn)制代碼確定。

NoEscape(沒(méi)有逃逸),一個(gè)可以進(jìn)行標(biāo)量替換的對(duì)象??梢圆粚⑦@種對(duì)象分配在傳統(tǒng)的堆上。

編譯器可以使用逃逸分析的結(jié)果,對(duì)程序進(jìn)行一下優(yōu)化。

堆分配對(duì)象變成棧分配對(duì)象。一個(gè)方法當(dāng)中的對(duì)象,對(duì)象的引用沒(méi)有發(fā)生逃逸,那么這個(gè)方法可能會(huì)被分配在棧內(nèi)存上而非常見(jiàn)的堆內(nèi)存上。

消除同步。線程同步的代價(jià)是相當(dāng)高的,同步的后果是降低并發(fā)性和性能。逃逸分析可以判斷出某個(gè)對(duì)象是否始終只被一個(gè)線程訪問(wèn),如果只被一個(gè)線程訪問(wèn),那么對(duì)該對(duì)象的同步操作就可以轉(zhuǎn)化成沒(méi)有同步保護(hù)的操作,這樣就能大大提高并發(fā)程度和性能。

矢量替代。逃逸分析方法如果發(fā)現(xiàn)對(duì)象的內(nèi)存存儲(chǔ)結(jié)構(gòu)不需要連續(xù)進(jìn)行的話,就可以將對(duì)象的部分甚至全部都保存在CPU寄存器內(nèi),這樣能大大提高訪問(wèn)速度。

下面,我們看一下逃逸分析的例子。

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; 
} 
}

在這個(gè)例子當(dāng)中,我們創(chuàng)建了兩個(gè)對(duì)象,F(xiàn)oo對(duì)象和Bar對(duì)象,同時(shí)我們把Foo對(duì)象的應(yīng)用賦值給了Bar對(duì)象的方法。此時(shí),如果Bar對(duì)在堆上就會(huì)引起Foo對(duì)象的逃逸,但是,在本例當(dāng)中,編譯器通過(guò)逃逸分析,可以知道Bar對(duì)象沒(méi)有逃出example()方法,因此這也意味著Foo也沒(méi)有逃出example方法。因此,編譯器可以將這兩個(gè)對(duì)象分配到棧上。

1.4 編譯器經(jīng)過(guò)逃逸分析的效果

測(cè)試代碼:

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運(yùn)行參數(shù):

未開(kāi)啟逃逸分析設(shè)置為:

-server -verbose:gc

開(kāi)啟逃逸分析設(shè)置為:

-server -verbose:gc -XX:+DoEscapeAnalysis

在未開(kāi)啟逃逸分析的狀況下運(yùn)行情況如下:

[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

開(kāi)啟逃逸分析的狀況下,運(yùn)行情況如下:

Time cost is 10031306

未開(kāi)啟逃逸分析時(shí),運(yùn)行上訴代碼,JVM執(zhí)行了GC操作,而在開(kāi)啟逃逸分析情況下,JVM并沒(méi)有執(zhí)行GC操作。同時(shí),操作時(shí)間上,開(kāi)啟逃逸分析的程序運(yùn)行時(shí)間是未開(kāi)啟逃逸分析時(shí)間的1/5。

2. TLAB

JVM在內(nèi)存新生代Eden Space中開(kāi)辟了一小塊線程私有的區(qū)域,稱(chēng)作TLAB(Thread-local allocation buffer)。默認(rèn)設(shè)定為占用Eden Space的1%。在Java程序中很多對(duì)象都是小對(duì)象且用過(guò)即丟,它們不存在線程共享也適合被快速GC,所以對(duì)于小對(duì)象通常JVM會(huì)優(yōu)先分配在TLAB上,并且TLAB上的分配由于是線程私有所以沒(méi)有鎖開(kāi)銷(xiāo)。因此在實(shí)踐中分配多個(gè)小對(duì)象的效率通常比分配一個(gè)大對(duì)象的效率要高。

也就是說(shuō),Java中每個(gè)線程都會(huì)有自己的緩沖區(qū)稱(chēng)作TLAB(Thread-local allocation buffer),每個(gè)TLAB都只有一個(gè)線程可以操作,TLAB結(jié)合bump-the-pointer技術(shù)可以實(shí)現(xiàn)快速的對(duì)象分配,而不需要任何的鎖進(jìn)行同步,也就是說(shuō),在對(duì)象分配的時(shí)候不用鎖住整個(gè)堆,而只需要在自己的緩沖區(qū)分配即可。

關(guān)于對(duì)象分配的JDK源碼可以參見(jiàn)JVM 之 Java對(duì)象創(chuàng)建[初始化]中對(duì)OpenJDK源碼的分析。

3. Java對(duì)象分配的過(guò)程

編譯器通過(guò)逃逸分析,確定對(duì)象是在棧上分配還是在堆上分配。如果是在堆上分配,則進(jìn)入選項(xiàng)2.

如果tlab_top + size <= tlab_end,則在在TLAB上直接分配對(duì)象并增加tlab_top 的值,如果現(xiàn)有的TLAB不足以存放當(dāng)前對(duì)象則3.

重新申請(qǐng)一個(gè)TLAB,并再次嘗試存放當(dāng)前對(duì)象。如果放不下,則4.

在Eden區(qū)加鎖(這個(gè)區(qū)是多線程共享的),如果eden_top + size <= eden_end則將對(duì)象存放在Eden區(qū),增加eden_top 的值,如果Eden區(qū)不足以存放,則5.

執(zhí)行一次Young GC(minor collection)。

經(jīng)過(guò)Young GC之后,如果Eden區(qū)任然不足以存放當(dāng)前對(duì)象,則直接分配到老年代。

對(duì)象不在堆上分配主要的原因還是堆是共享的,在堆上分配有鎖的開(kāi)銷(xiāo)。無(wú)論是TLAB還是棧都是線程私有的,私有即避免了競(jìng)爭(zhēng)(當(dāng)然也可能產(chǎn)生額外的問(wèn)題例如可見(jiàn)性問(wèn)題),這是典型的用空間換效率的做法。

Java中對(duì)象是不是都分配在堆上

到此,相信大家對(duì)“Java中對(duì)象是不是都分配在堆上”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢(xún),關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI