柚子快報(bào)邀請碼778899分享:java JVM 堆空間
柚子快報(bào)邀請碼778899分享:java JVM 堆空間
概述
一個(gè) JVM 實(shí)例中只存在一個(gè)堆內(nèi)存,是 Java 內(nèi)存管理的核心區(qū)域。在 JVM 啟動(dòng)時(shí)創(chuàng)建堆?!禞ava 虛擬機(jī)規(guī)范》規(guī)定,堆可以處于物理上不連續(xù)的內(nèi)存空間中,但是在邏輯上它應(yīng)該被視為連續(xù)的。所有的線程共享 Java 堆,在這里還可以劃分線程私有的緩沖區(qū)(Thread Local Allocation Buffer,TLAB)幾乎所有的對象實(shí)例以及數(shù)組都應(yīng)當(dāng)在運(yùn)行時(shí)分配在堆上,所以在棧幀上只保存對象實(shí)例和數(shù)組的引用。在方法結(jié)束后,堆中的對象不會(huì)馬上被移除,僅僅在垃圾回收時(shí)才有可能被移除。堆上的數(shù)據(jù)是線程共享的,本線程用完后,無法確定其他線程是否還是使用(要確定其他線程是否在使用代價(jià)較高)。因此需要等到離線 任務(wù)(垃圾回收)去執(zhí)行這個(gè)耗時(shí)較長的確認(rèn)過程。堆是 GC(Garbage Collection,垃圾收集器)執(zhí)行垃圾回收的重點(diǎn)區(qū)域。由于 Method Area 和 Heap Area 內(nèi)存沒有過期策略,所以才誕生類 GC。Heap Area 數(shù)據(jù)變化比 Method Area 更加劇烈,有用戶線程創(chuàng)建很多數(shù)據(jù),因此也可以 GC就是為 Heap Area 而生的。
在下圖在與到 new、newarray、anewarray 指令時(shí)就會(huì)在堆中開辟空間創(chuàng)建對象。
內(nèi)存細(xì)分
現(xiàn)代大部分垃圾收集器都是基于分代收集理論設(shè)計(jì)的。
Java 7 之前堆內(nèi)存邏輯劃分為:
新生區(qū)(Young Generation Space): Young/New養(yǎng)老區(qū)(Tenure generation Space):Old/Tenure永久區(qū)(Permanent Space):Perm
Java 8 之前堆內(nèi)存邏輯劃分為:
新生區(qū)(Young Generation Space): Young/New
又被劃分為 Eden 區(qū)和 Survivor 區(qū) 養(yǎng)老區(qū)(Tenure generation Space):Old/Tenure元空間(Meta Space):Mate
約定:
新生區(qū)
?
\Leftrightarrow
? 新生代
?
\Leftrightarrow
? 年輕代養(yǎng)老區(qū)
?
\Leftrightarrow
? 老年區(qū)
?
\Leftrightarrow
? 老年代永久區(qū)
?
\Leftrightarrow
? 永久代
堆空間內(nèi)部結(jié)構(gòu)(JDK 7)
如下圖:我們限制 JVM 的??臻g為 10 M,通過下圖可以看到:
伊甸園:2 MS0區(qū):0.5 MS1區(qū):0.5 M老年代:7 M
一共 10 M
public static void main(String[] args) {
System.out.println("start...");
try {
Thread.sleep(1000000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("end...");
}
設(shè)置堆內(nèi)存大小與 OOM
設(shè)置堆內(nèi)存大小的參數(shù)
“-Xms”:用于表示堆區(qū)的起始內(nèi)存,等價(jià)于-XX:InitialHeapSize。-X:是 JVM 的運(yùn)行參數(shù)。ms:memory start“-Xmx”:用于表示堆區(qū)的最大內(nèi)存,等價(jià)于-XX:MaxHeapSize。
一旦堆區(qū)中內(nèi)存大小超過 “-Xmx” 所指定的最大內(nèi)存是,將會(huì)拋出 OutOfMemoryError 異常。
通常 會(huì)將 -Xms 和 -Xmx 兩個(gè)參數(shù)配置相同的值,其目的是為了能夠在 Java 垃圾回收機(jī)制清理完堆區(qū)后不需要重新分割計(jì)算堆區(qū)的大小,從而提供性能。
默認(rèn)情況下,
初始內(nèi)存大小:電腦物理內(nèi)存大小 / 64最大內(nèi)存大?。弘娔X物理內(nèi)存大小 / 4
public static void main(String[] args) {
// 返回 Java 虛擬機(jī)中堆內(nèi)存總量(當(dāng)前堆大小:初始化內(nèi)存)
long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
// 返回 Java 虛擬機(jī)試圖使用的最大內(nèi)存量
long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
System.out.println("-Xms:" + initialMemory + "M");
System.out.println("-Xmx:" + maxMemory + "M");
System.out.println("系統(tǒng)內(nèi)存大小為:" + initialMemory * 64.0 / 1024 + "G");
System.out.println("系統(tǒng)內(nèi)存大小為:" + maxMemory * 4.0 / 1024 + "G");
}
/*
-Xms:245M
-Xmx:3641M
系統(tǒng)內(nèi)存大小為:15.3125G
系統(tǒng)內(nèi)存大小為:14.22265625G
*/
查看設(shè)置參數(shù)
方法一:jstat -gc xxxx
S0C:S0 區(qū)的最大內(nèi)存量S0U:S0 區(qū)的當(dāng)前使用內(nèi)存量EC:伊甸園的最大內(nèi)存量OC:老年代的最大內(nèi)存量
S0 與 S1 在使用時(shí),只會(huì)使用其中一個(gè),所以只需要計(jì)算其中一個(gè)。
方法二:使用 +PrintGCDetails 參數(shù)
年輕代與老年代
JVM 中的 Java 對象可以根據(jù)生命周期長短劃分成兩類:
生命周期較短的對象,這類對象的創(chuàng)建和消亡非常迅速,朝生夕死。這類對象的個(gè)數(shù)占所有所有對象個(gè)數(shù)的 80%,有必要單獨(dú)劃出來一個(gè)區(qū),單獨(dú)處理。另一類對象生命周期非常長,在某些極端情況下還能夠與 JVM 的生命周期保持一致。這類對象如果每次 GC,都去判斷,非常浪費(fèi)性能。
下邊這參數(shù)開發(fā)中一般不會(huì)調(diào)
配置新生代與老年代在堆結(jié)構(gòu)中的占比
默認(rèn) -XX:NewRatio=2,表示新生代占 1,老年代占 2,新生代占整個(gè)堆的 1/3可以修改 -XX:NewRatio=4,表示新生代占 1,老年代占 4,新生代占整個(gè)堆的 1/5
在 HotSpot 中,Eden 空間和另外兩個(gè) Survivor 空間缺省占比的比例是:8:1:1
可以通過“ -XX:SurvivorRation ” 調(diào)整這個(gè)空間的比例。
幾乎所有 Java 對象都是在 Eden 區(qū)被 new 出來的。
可以使用 “-Xmn” 設(shè)置新生代最大內(nèi)存大?。海ㄒ话闶褂媚J(rèn)值),如果 -Xmn 與 -XX:NewRatio=2 出現(xiàn)矛盾,那么以 -Xmn 為準(zhǔn),因?yàn)?-Xmn 明確指定新生代的內(nèi)存大小。
如下圖:最大堆空間為 600M。按照之前的說法:新生代與老年代是1:2,那老年代是
600
?
2
3
=
400
600*\frac{2}{3}=400
600?32?=400 M,如下圖沒有問題。
而:Eden 空間和另外兩個(gè) Survivor 空間缺省占比的比例是:8:1:1,那么Eden 應(yīng)該是:
200
?
0.8
=
160
M
200*0.8=160M
200?0.8=160M ,s0 和 s1 各占 20 M。
但真實(shí)情況:
老年代是 400 M 沒有問題Eden 區(qū)是150Ms0 和 s1 各占 25 M此時(shí)的比例:6:1:1 ,與《Java 虛擬機(jī)規(guī)范》規(guī)定不一致,是因?yàn)?JVM 有自適應(yīng)機(jī)制,如果要嚴(yán)格執(zhí)行 8:1:1 ,需要手動(dòng)指定:-XX:SurvivorRation=8
對象分配過程
為對象分配內(nèi)存是一件非常嚴(yán)謹(jǐn)和復(fù)雜的任務(wù),JVM 的設(shè)計(jì)者們不僅需要考慮內(nèi)存如何分配、在哪里分配等問題,并且由于內(nèi)存分配算法與內(nèi)存垃圾回收算法密切相關(guān),所以還需要考慮 GC 執(zhí)行完內(nèi)存垃圾回收后是否在內(nèi)存空間中產(chǎn)生內(nèi)存碎片。
對象分配的一般過程
new 的對象先放到伊甸園區(qū)。此區(qū)有大小限制。當(dāng)伊甸園區(qū)填滿時(shí),程序有需要?jiǎng)?chuàng)建對象,JVM 的垃圾回收器將對伊甸園區(qū)進(jìn)行垃圾回收(Minor GC),將伊甸園區(qū)中的不再被其他對象所引用的對象進(jìn)行銷毀。再加載新的對象放到伊甸園區(qū)。然后將伊甸園中的剩余對象移動(dòng)到幸存者 0 區(qū)。如果再次觸發(fā)垃圾回收,上次幸存下來的放到幸存者 0 區(qū)的對象,如果沒有回收,就會(huì)放到幸存者 1區(qū),并清空幸存者 0 區(qū)。如果再次經(jīng)歷垃圾回收,會(huì)重新將幸存者放回幸存者 0 區(qū),接著再去幸存者 1 區(qū)。啥時(shí)候能去養(yǎng)老區(qū)呢?可以設(shè)置次數(shù):默認(rèn)值是 15 次。
設(shè)置參數(shù):-XX:MaxTenuringThreshold=16 進(jìn)行設(shè)置 在養(yǎng)老區(qū),相對悠閑。當(dāng)養(yǎng)老區(qū)內(nèi)存不足時(shí),再次出發(fā) GC:Major GC,進(jìn)行養(yǎng)老區(qū)的內(nèi)存清理。若養(yǎng)老區(qū)執(zhí)行了 Major GC 之后發(fā)現(xiàn)依然無法進(jìn)行對象的保存,就會(huì)產(chǎn)生 OOM 異常。
new 對象先放到伊甸園區(qū),當(dāng)伊甸園區(qū)滿了,觸發(fā)YGC。伊甸園區(qū)內(nèi)的垃圾對象直接清除,不是垃圾對象遷移到 S0(to 區(qū):S0 和 S1 哪個(gè)區(qū)為空,哪個(gè)是 to 區(qū)) 區(qū),記錄這些對象的生命值為 1。YGC 后,伊甸園區(qū)為空,S0 和 S1 某一個(gè)區(qū)為空。
當(dāng)再次出發(fā) YGC 時(shí),S1 為空時(shí) to 區(qū),那么將伊甸園區(qū)和 S0 區(qū)的有效對象遷移到 S1 區(qū)(to 區(qū)),同事這些對象的生命值+1。在 YGC 時(shí)如果from 區(qū)中有對象失效了(比如對象 A),也需要清除。
這次 YGC 有些特殊,因?yàn)樵?S1 區(qū)中有些對象的生命值到達(dá) 15(默認(rèn)值)時(shí),需要這些對象升級到老年代(對象H 和 D)。通過 -XX:MaxTenuringThreshold=16 進(jìn)行修改進(jìn)入老年代的閾值。
注意:
只有伊甸園區(qū)滿時(shí)才會(huì)觸發(fā) YGC。S0 和 S1 區(qū)滿時(shí)不會(huì)觸發(fā) YGC。
總結(jié):
針對幸存者 S0、S1 區(qū)的總結(jié):復(fù)制之后有交換,誰空誰是 to關(guān)于垃圾回收:頻繁在新生代收集,很少在老年代收集,幾乎不在永久區(qū)(元空間)收集。
對象分配的特殊情況
特殊情況
在伊甸園區(qū) YGC 后,空間還是不夠存放一個(gè)實(shí)例對象,那么直接將該對象晉升到老年代。如果此時(shí)老年代空間也不夠,那么觸發(fā) FGC,F(xiàn)GC 后如果老年代空間足夠,直接將對象存儲(chǔ)在老年代;如果空間依然不夠,直接報(bào) OOM 異常。在YGC 時(shí),由于伊甸園區(qū)的空間大于 to 區(qū)(S0 或者 S1) ,如果有一個(gè)對象要往 to 區(qū)遷移時(shí), to 區(qū)沒有足夠的空間,那么將這個(gè)對象晉升老年代。
如下圖:
在黃色框里,可以看到有 3 次 YGC。 紅色框里時(shí)伊甸園區(qū)的使用情況,在 GC 前數(shù)據(jù)一直在增長,YGC 后伊甸園區(qū)被清空。 藍(lán)色框里時(shí)幸運(yùn)者區(qū)的使用情況,S1 和 S0 交替使用,交替被清空,在沒有 YGC 數(shù)據(jù)是不變的。 藍(lán)色框里時(shí)老年代的使用情況,在每次 YGC 后老年代的數(shù)據(jù)都在階梯式增長。
根據(jù)跑代碼實(shí)際的情況來看,是符合上邊理論的分析的。
/**
* -Xms600m -Xmx600m
*/
public class HeapInstanceTest {
byte[] buffer = new byte[new Random().nextInt(1024 * 200)];
public static void main(String[] args) throws InterruptedException {
List
while (true) {
list.add(new HeapInstanceTest());
Thread.sleep(10);
}
}
}
GC
GC 按照回收區(qū)域分為兩大類
不分收集
新生代收集(Minor GC / Young GC):只是新生代的垃圾收集。老年代收集(Manor GC / Old GC):只是老年代的垃圾收集。
目前,只有 CMS GC 會(huì)有單獨(dú)收集老年代的行為注意:很多時(shí)候 Major GC 會(huì)和 Full GC 混淆 ,需要具體分辨是老年代回收還是整堆回收。 混合收集(Mixed GC):收集整個(gè)新生代以及部分老年代的垃圾收集。
目前,只有 G1 GC 會(huì)有這種行為 整堆收集(Full GC):收集整個(gè) java 堆和方法區(qū)的垃圾收集。
新生代GC(Minor GC)觸發(fā)機(jī)制
當(dāng) Eden 代空間不足時(shí),就會(huì)觸發(fā) Minor GC。Survivor 滿不會(huì)引發(fā) GC。因?yàn)?Java 對象大多都具備朝生夕死的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。Minor GC 會(huì)引發(fā) STW(Stop The World),暫停其他用戶的線程,等垃圾回收結(jié)束,用戶線程才恢復(fù)運(yùn)行。
老生代GC(Major GC/ Full GC)觸發(fā)機(jī)制
出現(xiàn)了 Major GC,經(jīng)常會(huì)伴隨至少一次的 Minor GC(但非絕對的,在 Parallel Scavenge 收集器的收集策略里就有直接進(jìn)行 Major GC 策略選擇)
也就是在老年代空間不足時(shí),會(huì)先嘗試觸發(fā) Minor GC。如果之后空間還不足,則觸發(fā) Major GC。 Major GC 的速度一般比 Minor GC 慢 10 倍以上,STW 的時(shí)間更長。因此線上服務(wù)盡量減少 Major GC(Full GC)如果 Major GC 后,內(nèi)存還不足,就報(bào) OOM
Full GC 觸發(fā)機(jī)制
調(diào)用 System.gc() 時(shí),系統(tǒng)建議執(zhí)行 Full GC,但是不必然執(zhí)行。老年代空間不足方法區(qū)空間不足通過 Minor GC 后,進(jìn)入老年代的平均大小大于老年代的可用內(nèi)存。有 Eden 區(qū)、from 區(qū)向 to 區(qū)復(fù)制時(shí),對象大小大于 to 區(qū)可用內(nèi)存,則把該對象轉(zhuǎn)存到老年代,且老年代的可用內(nèi)存大小小于該對象大小。
full GC 是開發(fā)或調(diào)優(yōu)中盡量避免的
GC 日志分析:
發(fā)生了三次 YGC 后,才發(fā)生 Full GC。[PSYoungGen: 1875K->496K(2560K)] 表示:YGC 前的新生代占空空間 --> YGC 后的新生代占空空間 ( 新生代總空間 )Full GC,會(huì)對新生代、老年代、元空間進(jìn)行垃圾收集。最后一次 Full GC,從綠框看:Full GC 前與 Full GC 之后老年代內(nèi)存空間幾乎沒有變化,所以新對象需要占用的空間大于老年代剩余空間,所以Full GC 之后報(bào) OOM
/**
* -Xms9m -Xmx9m -XX:+PrintGCDetails
*/
public class GCTest {
public static void main(String[] args) {
int i = 0;
try {
List
String str = "dyf";
while (true) {
list.add(str);
str += str;
i++;
}
} catch (Throwable e) {
e.printStackTrace();
System.out.printf("遍歷次數(shù):" + i);
}
}
}
內(nèi)存分配策略
針對不同年齡段的對象分配原則
優(yōu)先分配到 Eden大對象直接分配到老年代
盡量避免程序中出現(xiàn)過多的大對象 長期存活的對象分配到老年代動(dòng)態(tài)對象年齡判斷
如果 Survivor 區(qū)中相同年齡的所有對象大小的總和大于 Survivor 空間的一半,年齡大于等于該年齡的對象可以直接進(jìn)入老年代,無須等到 MaxTenuringThreshold 中要求的年齡。因?yàn)槊看螐?from 區(qū)向 to 區(qū)拷貝數(shù)據(jù)時(shí),這些相同年齡的對象如果非常多,那么拷貝時(shí)非常耗性能。 空間分配擔(dān)保
-XX:HandlePromotionFailure
測試大對象之間進(jìn)入老年代
下邊代碼:
堆空間為 60 M-XX:NewRatio=2:新生代占堆空間的 1/3,那么新生代空間為:20M;老年代空間為 40M-XX:SurvivorRatio-8:Eden : S0 : S1 = 8 : 1 : 1,那么Eden 區(qū)空間為:16M,S0 和 S1 區(qū)空間為: 2 Mbyte[] buffer 空間為 20M,Eden 和 S0 都放不下,符合直接晉升老年代的條件。
/**
* -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio-8 -XX:+PrintGCDetails
*/
public class YoungOldAreaTest {
public static void main(String[] args) {
// 20 m
byte[] buffer = new byte[1024 * 1024 * 20];
}
}
為對象分配內(nèi)存:TLAB
TLAB 是(Thread Local Allocation Buffer)為每個(gè)線程分配一個(gè)緩存區(qū)。
TLAB 誕生的背景
堆區(qū)是線程共享區(qū)域,任何線程都可以訪問到堆區(qū)中的共享的數(shù)據(jù)。由于對象實(shí)例的創(chuàng)建在 JVM 中非常頻繁,因此在并發(fā)環(huán)境下從堆區(qū)中劃分內(nèi)存空間是線程不安全的。為了避免多個(gè)線程操作統(tǒng)一地址,需要使用加鎖機(jī)制,進(jìn)而影響分配速度。
什么是 TLAB
從內(nèi)存模型而不是垃圾收集的角度,對 Eden 區(qū)繼續(xù)劃分,JVM 為每個(gè)線程分配一個(gè)私有緩存區(qū)域,它包含在 Eden 空間內(nèi)。多線程同事分配內(nèi)存時(shí),使用 TLAB 可以避免一系列的非線程安全問題,同時(shí)還能提升內(nèi)存分配的吞吐量,因此我們可以將這種內(nèi)存分配方案稱之為快速分配策略所有 OpenJDK 衍生出來的 JVM 都提供類 TLAB 的設(shè)計(jì)。盡管不是所有的對象實(shí)例都能夠在 TLAB 中成功分配內(nèi)存,但JVM 確實(shí)是將 TLAB 作為內(nèi)存分配的首選。在程序中,開發(fā)人員可以通過選項(xiàng) ”-XX:UseTLAB“ 設(shè)置是否開啟 TLAB 空間,默認(rèn)開啟。默認(rèn)情況下,TLAB 空間的內(nèi)存非常小,僅占整個(gè) Eden 空間的 1%,當(dāng)然我們可以通過選項(xiàng):”-XX:TLABWasteTargetPercent“ 設(shè)置 TLAB 空間所占用 Eden 空間的百分比大小。一旦對象在 TLAB 空間分配內(nèi)存失敗,JVM 就會(huì)嘗試通過使用加鎖機(jī)制確保數(shù)據(jù)操作的原子性,從而直接在 Eden 空間中分配內(nèi)存。
對象分配過程:TLAB
下圖 ”Eden 分配“ 如果分配失敗,會(huì)觸發(fā) YGC后,在進(jìn)行分配,如果還是失敗,會(huì)將對象晉升到老年代。
注意:由于 TLAB 的存在,堆空間不一定都是線程共享的。
堆空間的參數(shù)設(shè)置
官網(wǎng)地址:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html
-XX:+PrintFlagsInitial:查看所有的參數(shù)的默認(rèn)值-XX:+PrintFlagsFinal:查看所有參數(shù)的最終值(實(shí)際值:存在修改,不再是初始值)-Xms:初始堆空間內(nèi)存(默認(rèn)值為物理內(nèi)存的
1
64
\frac{1}{64}
641?)-Xmx:最大堆空間內(nèi)存(默認(rèn)值為物理內(nèi)存的
1
4
\frac{1}{4}
41?)-XX:NewRatio:配置新生代和老年代在堆結(jié)構(gòu)的占比-XX:SurvivorRatio:配置新生代中 Eden 和 S0/S1 空間比例-XX:MaxTenuringThreshold:設(shè)置新生代的最大年齡-XX:+PrintGCDetails:輸出詳細(xì)的 GC 處理日志打印 GC 簡要信息
-XX:+PrintGC-verbose:gc -XX:handlePromotionFailure:是否設(shè)置空間分配擔(dān)保
NewRatio
NewRatio 設(shè)置如果比較的大,那么 Eden 區(qū)比較大。此時(shí)當(dāng)出現(xiàn) YGC 時(shí),那些幸存下來的對象 S0/S1 放不下,會(huì)將大量的對象晉升到老年代,導(dǎo)致老年代空間占用較多,引起頻繁 Full GC。
NewRatio 設(shè)置如果比較的小,那么 Eden 區(qū)太小。Eden 很容易滿,引起頻繁 YGC。
handlePromotionFailure
在發(fā)生 Minor GC 之前,虛擬機(jī)會(huì)檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象的總空間。
如果大于,則此次 Minor GC 時(shí)安全的如果小于,則虛擬機(jī)會(huì)查看 -XX:HandlePromotionFailure 設(shè)置只是否允許擔(dān)保失敗
如果 HandlePromotionFailure = true,那么會(huì)繼續(xù)檢查老年代最大可用連續(xù)空間是否大于歷次晉升老年代對象的平均大小。
如果大于,則嘗試進(jìn)行一次 Minor GC,但這次 Minor GC 依然有失敗的風(fēng)險(xiǎn)。如果小于,則改為進(jìn)行一次 Full GC 如果 HandlePromotionFailure = false,則改為進(jìn)行一次 Full GC
JDK 7 之后 HandlePromotionFailure 參數(shù)不會(huì)影響到虛擬機(jī)的空間分配擔(dān)保策略(JDK7 HandlePromotionFailure 永遠(yuǎn)為 true)。JDK 7 之后:只要老年代的連續(xù)空間大于新生代對象總大小或者歷次晉升的平均大小就會(huì)進(jìn)行 Minor GC,否則將進(jìn)行 Full GC。
堆是分配對象的唯一選擇嗎?
隨著 JIT 編譯期的發(fā)展與逃逸分析技術(shù)逐漸成熟,棧上分配、標(biāo)量替換優(yōu)化技術(shù)將會(huì)導(dǎo)致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那么 ”絕對“ 了。
如果經(jīng)過逃逸分析(Escape Analysis)后發(fā)現(xiàn),一個(gè)對象bing沒有逃逸出方法的話,那么就可能被優(yōu)化成棧上分配。
基于 OpenJDK 深度定制的 TaoBaoVM,其中創(chuàng)新的 GCIH(GC invisible heap)技術(shù)實(shí)現(xiàn) off-heap,將生命周期較長的 Java 對象從 heap 中移至 heap 外,并且 GC 不能管理 GCIH 內(nèi)部的 Java 對象(法外之地),以此達(dá)到降低 GC 的回收頻率和提升 GC 的回收效率的目的。
逃逸分析
逃逸分析的基本行為就是分析對象動(dòng)態(tài)作用域
當(dāng)一個(gè)對象在方法中定義后,對象只在方法內(nèi)部使用,則認(rèn)為沒有發(fā)生逃逸。當(dāng)一個(gè)對象在方法中定義后,它被外部方法引用,則認(rèn)為發(fā)生了逃逸。例如:作為調(diào)用參數(shù)傳遞到其他方法中。
沒有放生逃逸的對象,則可以分配到棧上,隨著方法執(zhí)行的結(jié)束,棧空間就被移除。棧有自動(dòng)過期(出棧)機(jī)制。棧幀線程私有,如果對象沒有逃逸,那么它的作用域就在棧幀內(nèi)部,不可能共享,可以隨著棧幀而生,隨著棧幀而亡。
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(s1);
stringBuffer.append(s2);
// stringBuffer 對象發(fā)生逃逸,返回出去它作用域也就出方法區(qū)了
return stringBuffer;
}
public static String createStringBuffer2(String s1, String s2) {
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append(s1);
stringBuffer.append(s2);
// stringBuffer 對象沒有發(fā)生逃逸,方法執(zhí)行完畢,stringBuffer 也就煙消云散了
return stringBuffer.toString();
}
// 判斷是否發(fā)生逃逸,就看對象是否有可能在方法外調(diào)用(是不是方法私有)
public class EscapeAnalysis {
// 這個(gè)實(shí)體不屬于某個(gè)方法私有,它不可能放到棧幀里
public EscapeAnalysis obj;
// 方法返回 EscapeAnalysis 對象,發(fā)生逃逸
public EscapeAnalysis getInstance() {
return obj == null ? new EscapeAnalysis() : obj;
}
// 為成員變量賦值,發(fā)生逃逸
public void setObj() {
this.obj = new EscapeAnalysis();
}
// 對象的作用域僅在當(dāng)前方法中有效,沒有發(fā)生逃逸
public void useEscapeAnalysis() {
EscapeAnalysis e = new EscapeAnalysis();
}
// 引用成員變量的值,發(fā)生逃逸
public void useEscapeAnalysis1() {
EscapeAnalysis e = getInstance();
}
}
JDK 7 以后 HotSpot 中默認(rèn)就已經(jīng)開啟了逃逸分析。
結(jié)論:開發(fā)中能使用局部變量的,就不要定義在方法外。
對于沒有逃逸對象進(jìn)行代碼優(yōu)化
棧上分配。將對分配轉(zhuǎn)化為棧分配同步省略。如果發(fā)現(xiàn)一個(gè)對象只能被一個(gè)線程訪問到,那么對于這個(gè)對象的操作可以不考慮同步了。分離對象或標(biāo)量替換。有的對象可能不需要作為一個(gè)連續(xù)的內(nèi)存結(jié)構(gòu)存在,也可以被訪問到,那么對象的部分(或者全部)可以不存儲(chǔ)在內(nèi)存,而是存儲(chǔ) Java 棧中。
棧上分配測試
當(dāng)不使用棧上分配時(shí),使用了4G堆空間,調(diào)用 100000000 次方發(fā)生了 YGC,YGC 之后堆中有 User 對象:34306167,程序耗時(shí):701ms。
當(dāng)使用棧上分配時(shí),使用了1G堆空間,調(diào)用 100000000 次方?jīng)]發(fā)生了 YGC,內(nèi)存中有 User 對象:84729,程序耗時(shí):6ms。
標(biāo)量替換
標(biāo)量(Scalar)是指一個(gè)無法在分解成更小的數(shù)據(jù)。Java 中的原始數(shù)據(jù)類型就是標(biāo)量。
相對標(biāo)量,那些可以分解的數(shù)據(jù)叫聚合量。Java 對象就是聚合量。
在 JIT 階段,經(jīng)過逃逸分析,發(fā)現(xiàn)一個(gè)不逃逸對象,經(jīng)過 JIT 優(yōu)化,就會(huì)把這個(gè)對象拆解成若干個(gè)標(biāo)量來替換成員變量,這個(gè)過程就是標(biāo)量替換
public static void alloc1() {
// point 對象沒有發(fā)生逃逸
Point point = new Point(1, 2);
System.out.println("point.x=" + point.x + ";point.y=" + point.y);
}
static class Point {
public int x;
public int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}
上邊代碼經(jīng)過標(biāo)量替換后。
這樣可以大大減少堆內(nèi)存的占用,標(biāo)量替換為棧上分配提供了很好的基礎(chǔ)。
// x 和 y 存在棧幀的局部變量表中
public static void alloc() {
int x = 1;
int y = 2;
System.out.println("point.x=" + x + ";point.y=" + y);
}
參數(shù):-XX:+EliminateAllocations 開啟標(biāo)量替換(默認(rèn)打開),允許將對象打散分配在棧上。
標(biāo)量替換代碼測試
未打開標(biāo)量替換
大量 YGC耗時(shí)長:438ms
打開標(biāo)量替換
沒有 GC耗時(shí)短:5ms
逃逸分析小結(jié):逃逸分析并不成熟
無法保證逃逸分析的性能消耗一定能高于他的消耗。雖然經(jīng)過逃逸分析可以做標(biāo)量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進(jìn)行一系列復(fù)雜的分析的,這其實(shí)也是一個(gè)相對耗時(shí)的過程。
一個(gè)極端的例子,就是經(jīng)過逃逸分析之后,發(fā)現(xiàn)沒有一個(gè)對象是不逃逸的。那這個(gè)逃逸分析的過程就白白浪費(fèi)掉了。
雖然這項(xiàng)技術(shù)并不十分成熟,但是它也是即時(shí)編譯器優(yōu)化技術(shù)中一個(gè)十分重要的手段。
注意到有一些觀點(diǎn),認(rèn)為通過逃逸分析,JVM會(huì)在棧上分配那些不會(huì)逃逸的對象,這在理論上是可行的,但是取決于JVM設(shè)計(jì)者的選擇。據(jù)我所知,Oracle Hotspot JVM中并未這么做,這一點(diǎn)在逃逸分析相關(guān)的文檔里已經(jīng)說明,所以可以明確所有的對象實(shí)例都是創(chuàng)建在堆上。
目前很多書籍還是基于JDK7以前的版本,JDK已經(jīng)發(fā)生了很大變化,intern字符串的緩存和靜態(tài)變量曾經(jīng)都被分配在永久代上,而永久代已經(jīng)被元數(shù)據(jù)區(qū)取代。但是,intern字符串緩存和靜態(tài)變量并不是被轉(zhuǎn)移到元數(shù)據(jù)區(qū),而是直接在堆上分配,所以這一點(diǎn)同樣符合前面一點(diǎn)的結(jié)論:對象實(shí)例都是分配在堆上。
柚子快報(bào)邀請碼778899分享:java JVM 堆空間
推薦文章
本文內(nèi)容根據(jù)網(wǎng)絡(luò)資料整理,出于傳遞更多信息之目的,不代表金鑰匙跨境贊同其觀點(diǎn)和立場。
轉(zhuǎn)載請注明,如有侵權(quán),聯(lián)系刪除。