優(yōu)質(zhì)博文:IT-BLOG-CN
面試管坑位:在Java
中新創(chuàng)建的對(duì)象一定是在堆上分配內(nèi)存嗎?如果你的答案是“是的”那就需要看看這個(gè)文章了。
一、簡(jiǎn)介
逃逸分析Escape Analysis
:是一個(gè)很重要的JIT
優(yōu)化技術(shù),用于判斷對(duì)象是否會(huì)在方法外部被訪問(wèn)到,也就是逃出方法的作用域。逃逸分析是JIT
編譯器的一個(gè)步驟,通過(guò)JIT
我們能夠確定哪些對(duì)象可以被限制在方法內(nèi)部使用,不會(huì)逃逸到外部,然后可以對(duì)它們進(jìn)行優(yōu)化,比如把它們分配在棧上而不是堆上,或者進(jìn)行標(biāo)量替換,把一個(gè)對(duì)象拆散成多個(gè)基本類型來(lái)存儲(chǔ)。是一種可以有效減少Java
程序中同步負(fù)載和內(nèi)存堆分配和垃圾回收壓力的跨函數(shù)全局?jǐn)?shù)據(jù)流分析算法。通過(guò)逃逸分析,Java Hotspot
編譯器能夠分析出一個(gè)新對(duì)象的引用的使用范圍,而決定是否要將這個(gè)對(duì)象分配到堆上。
逃逸分析主要針對(duì)局部變量,判斷堆上分配的對(duì)象是否逃逸出方法的作用域。它同編譯器優(yōu)化原理的指針?lè)治龊屯庑畏治鱿嚓P(guān)聯(lián)。當(dāng)變量(或者對(duì)象)在方法中分配后,其指針有可能被返回或者被全局引用,這樣就會(huì)被其他方法或者線程所引用,這種現(xiàn)象稱作指針(或者引用)的逃逸Escape
。通俗點(diǎn)講,如果一個(gè)對(duì)象的指針被多個(gè)方法或者線程引用時(shí),那么我們就稱這個(gè)對(duì)象的指針發(fā)生了逃逸。合理地設(shè)計(jì)代碼結(jié)構(gòu)和數(shù)據(jù)的使用方式能夠更好地利用逃逸分析來(lái)優(yōu)化程序的性能。我們還可以通過(guò)逃逸分析減少堆上分配對(duì)象的開(kāi)銷,提高內(nèi)存利用率。
逃逸分析不是直接的優(yōu)化手段,而是代碼分析手段。
二、逃逸分析的好處
【1】棧上分配,可以降低垃圾收集器運(yùn)行的頻率。
【2】同步消除,如果發(fā)現(xiàn)某個(gè)對(duì)象只能從一個(gè)線程可訪問(wèn),那么在這個(gè)對(duì)象上的操作可以不需要同步。
【3】標(biāo)量替換,把對(duì)象分解成一個(gè)個(gè)基本類型,并且內(nèi)存分配不再是分配在堆上,而是分配在棧上。這樣的好處有:減少內(nèi)存使用,因?yàn)椴挥蒙蓪?duì)象頭。和程序內(nèi)存回收效率高,并且GC
頻率也會(huì)減少。因此對(duì)于臨時(shí)對(duì)象或短期使用的對(duì)象,盡量使用局部變量來(lái)存儲(chǔ),以減少對(duì)象逃逸的可能性。對(duì)于復(fù)雜的數(shù)據(jù)結(jié)構(gòu),盡量使用基本類型、數(shù)組或集合類,以減少對(duì)象的分配和逃逸。
【4】使用final
關(guān)鍵字來(lái)限制對(duì)象的可變性,這樣JIT
編譯器更容易進(jìn)行逃逸分析和優(yōu)化。
三、why
棧上分配Stack Allocations
:在Java
虛擬機(jī)中,Java
堆上分配創(chuàng)建對(duì)象的內(nèi)存空間幾乎是Java
程序員都知道的常識(shí),Java
堆中的對(duì)象對(duì)于各個(gè)線程都是共享和可見(jiàn)的,只要持有這個(gè)對(duì)象的引用,就可以訪問(wèn)到堆中存儲(chǔ)的對(duì)象數(shù)據(jù)。虛擬機(jī)的垃圾收集子系統(tǒng)會(huì)回收堆中不再使用的對(duì)象,但回收動(dòng)作無(wú)論是標(biāo)記篩選出可回收對(duì)象,還是回收和整理內(nèi)存,都需要耗費(fèi)大量資源。如果確定一個(gè)對(duì)象不會(huì)逃逸出線程之外,那讓這個(gè)對(duì)象在棧上分配內(nèi)存將會(huì)是一個(gè)很不錯(cuò)的主意,對(duì)象所占用的內(nèi)存空間就可以隨棧幀出棧而銷毀。在一般應(yīng)用中,完全不會(huì)逃逸的局部對(duì)象和不會(huì)逃逸出線程的對(duì)象所占的比例是很大的,如果能使用棧上分配,那大量的對(duì)象就會(huì)隨著方法的結(jié)束而自動(dòng)銷毀了,垃圾收集子系統(tǒng)的壓力將會(huì)下降很多。棧上分配可以支持方法逃逸,但不能支持線程逃逸。
標(biāo)量替換Scalar Replacement
: 若一個(gè)數(shù)據(jù)已經(jīng)無(wú)法再分解成更小的數(shù)據(jù)來(lái)表示了,Java
虛擬機(jī)中的原始數(shù)據(jù)類型都不能再進(jìn)一步分解,那么這些數(shù)據(jù)就可以被稱為標(biāo)量。相對(duì)的,如果一個(gè)數(shù)據(jù)可以繼續(xù)分解,那它就被稱為聚合量Aggregate
,Java
中的對(duì)象就是典型的聚合量。如果把一個(gè)Java
對(duì)象拆散,根據(jù)程序訪問(wèn)的情況,將其用到的成員變量恢復(fù)為原始類型來(lái)訪問(wèn),這個(gè)過(guò)程就稱為標(biāo)量替換。假如逃逸分析能夠證明一個(gè)對(duì)象不會(huì)被方法外部訪問(wèn),并且這個(gè)對(duì)象可以被拆散,那么程序真正執(zhí)行的時(shí)候?qū)⒖赡懿蝗?chuàng)建這個(gè)對(duì)象,而改為直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用的成員變量來(lái)代替。將對(duì)象拆分后,除了可以讓對(duì)象的成員變量在棧上(棧上存儲(chǔ)的數(shù)據(jù),很大機(jī)會(huì)被虛擬機(jī)分配至物理機(jī)器的高速寄存器中存儲(chǔ))分配和讀寫(xiě)之外,還可以為后續(xù)進(jìn)一步的優(yōu)化手段創(chuàng)建條件。標(biāo)量替換可以視作棧上分配的一種特例,實(shí)現(xiàn)更簡(jiǎn)單(不用考慮整個(gè)對(duì)象完整結(jié)構(gòu)的分配),但對(duì)逃逸程度的要求更高,它不允許對(duì)象逃逸出方法范圍內(nèi)。
同步消除Synchronization Elimination
: 線程同步本身是一個(gè)相對(duì)耗時(shí)的過(guò)程,如果逃逸分析能夠確定一個(gè)變量不會(huì)逃逸出線程,無(wú)法被其他線程訪問(wèn),那么這個(gè)變量的讀寫(xiě)肯定就不會(huì)有競(jìng)爭(zhēng),對(duì)這個(gè)變量實(shí)施的同步措施也就可以安全地消除掉。需要注意的是:這種情況針對(duì)的是synchronized
鎖,而對(duì)于Lock
鎖,則JVM
并不能消除。
代碼說(shuō)明: 但是在實(shí)際的應(yīng)用程序中,尤其是大型程序中反而發(fā)現(xiàn)實(shí)施逃逸分析可能出現(xiàn)效果不穩(wěn)定的情況,或分析過(guò)程耗時(shí)但卻無(wú)認(rèn)開(kāi)啟的選項(xiàng)。如果有需要用戶可以使用參數(shù)-XX:+DoEscapeAnalysis
來(lái)手動(dòng)開(kāi)啟逃逸分析法有效判別出非逃逸對(duì)象而導(dǎo)致性能下降。
public class EscapeTest {
/**
* JIT編譯時(shí)會(huì)對(duì)代碼進(jìn)行逃逸分析
* 并不是所有對(duì)象存放在堆區(qū),有的一部分存在線程??臻g
* Person沒(méi)有逃逸
*/
private static String alloc() {
Person person = new Person();
return person.toString();
}
/**
* 同步省略(鎖消除)JIT編譯階段優(yōu)化,JIT經(jīng)過(guò)逃逸分析之后發(fā)現(xiàn)無(wú)線程安全問(wèn)題,就會(huì)做鎖消除
*/
public void append(String str1, String str2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(str1).append(str2);
}
/**
* 標(biāo)量替換
*/
private static void test2() {
Point point = new Point(1,2);
System.out.println("point.x="+point.getX()+"; point.y="+point.getY());
// 編譯后的偽代碼,也就是常說(shuō)的內(nèi)聯(lián)后的樣子
// int x=1;
// int y=2;
// System.out.println("point.x="+x+"; point.y="+y);
}
}
四、結(jié)論
關(guān)于逃逸分析的研究論文早在1999
年就已經(jīng)發(fā)表,但直到JDK 6
,HotSpot
才開(kāi)始支持初步的逃逸分析,而且到現(xiàn)在這項(xiàng)優(yōu)化技術(shù)尚未足夠成熟,仍有很大的改進(jìn)余地。不成熟的原因主要是逃逸分析的計(jì)算成本非常高,甚至不能保證逃逸分析帶來(lái)的性能收益會(huì)高于它的消耗。如果要百分之百準(zhǔn)確地判斷一個(gè)對(duì)象是否會(huì)逃逸,需要進(jìn)行一系列復(fù)雜的數(shù)據(jù)流敏感的過(guò)程間分析,才能確定程序各個(gè)分支執(zhí)行時(shí)對(duì)此對(duì)象的影響。前面介紹即時(shí)編譯、提前編譯優(yōu)劣勢(shì)時(shí)提到了過(guò)程間分析這種大壓力的分析算法正是即時(shí)編譯的弱項(xiàng)。可以試想一下,如果逃逸分析完畢后發(fā)現(xiàn)幾乎找不到幾個(gè)不逃逸的對(duì)象,那這些運(yùn)行期耗用的時(shí)間就白白浪費(fèi)了,所以目前虛擬機(jī)只能采用不那么準(zhǔn)確,但時(shí)間壓力相對(duì)較小的算法來(lái)完成分析。這個(gè)在JIT優(yōu)化實(shí)戰(zhàn)中有說(shuō)明。
jdk7
開(kāi)始默認(rèn)開(kāi)啟逃逸分析。在Java
代碼運(yùn)行時(shí),可以通過(guò)JVM
參數(shù)指 定是否開(kāi)啟逃逸分析:
‐XX:+DoEscapeAnalysis //表示開(kāi)啟逃逸分析 (jdk1.8默認(rèn)開(kāi)啟)
‐XX:‐DoEscapeAnalysis //表示關(guān)閉逃逸分析。
‐XX:+EliminateAllocations //開(kāi)啟標(biāo)量替換(默認(rèn)打開(kāi))
‐XX:+EliminateLocks //開(kāi)啟鎖消除(jdk1.8默認(rèn)開(kāi)啟)
開(kāi)啟逃逸與關(guān)閉逃逸的區(qū)別:
【1】關(guān)閉逃逸:-XX:-DoEscapeAnalysis -XX:+PrintGC
long start = System.currentTimeMillis();
for(int i=0;i<5000000;i++){
newObject();
}
long end = System.currentTimeMillis();
System.out.println("耗時(shí)"+(end-start)+"毫秒");
Thread.sleep(100000);
結(jié)果:41毫秒,一次GC
并且有一百多萬(wàn)的垃圾回收。
[GC (Allocation Failure) 65536K->880K(251392K), 0.0013300 secs]
耗時(shí)41毫秒
num #instances #bytes class name
1: 1088834 17868374 java.lang.Object
【2】開(kāi)啟逃逸:-XX:+DoEscapeAnalysis -XX:+PrintGC
只有4
毫秒,沒(méi)有GC
,提高了快10
倍效率,并且堆中只有十幾萬(wàn)。逃逸了文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-742288.html
耗時(shí)4毫秒
num #instances #bytes class name
1: 14534 2734633 java.lang.Object
可以發(fā)現(xiàn)一個(gè)逃逸和沒(méi)逃逸的問(wèn)題,只要是對(duì)象有被方法外部或者全局引用到那肯定會(huì)存在逃逸。當(dāng)對(duì)象沒(méi)有發(fā)生逃逸的時(shí)候,虛擬機(jī)會(huì)對(duì)其進(jìn)行優(yōu)化。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-742288.html
到了這里,關(guān)于逃逸分析:解鎖性能的神秘鑰匙!的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!