本章節內容接上一章【JVM】堆內部划分與對象分配(八)
五、GC 垃圾回收器
5.1、分代收集思想
Minor GC、Major GC、Full GC
-
我們都知道,JVM的調優的一個環節,也就是垃圾收集,我們需要盡量的避免垃圾回收,因為在垃圾回收的過程中,容易出現STW(Stop the World)的問題,而 Major GC 和 Full GC出現STW的時間,是Minor GC的10倍以上
-
JVM在進行GC時,並非每次都對上面三個內存區域一起回收的,大部分時候回收的都是指新生代。針對Hotspot VM的實現,它里面的GC按照回收區域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(FullGC)
分代收集:
-
部分收集:不是完整收集整個Java堆的垃圾收集。其中又分為:
-
新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
-
老年代收集(Major GC/Old GC):只是老年代的圾收集。
-
目前,只有CMS GC會有單獨收集老年代的行為。
-
注意,很多時候Major GC會和Full GC混淆使用,需要具體分辨是老年代回收還是整堆回收。
-
-
混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集。
-
目前,只有G1 GC會有這種行為
-
-
-
整堆收集(Full GC):收集整個java堆和方法區的垃圾收集。
5.2、Young GC
年輕代 GC(Minor GC)觸發機制
-
當年輕代空間不足時,就會觸發Minor GC,這里的年輕代滿指的是Eden代滿,Survivor滿不會引發GC。(每次Minor GC會清理年輕代的內存)
-
因為Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。這一定義既清晰又易於理解。
-
Minor GC會引發STW,暫停其它用戶的線程,等垃圾回收結束,用戶線程才恢復運行
5.3、Major/Full GC
老年代 GC(MajorGC/Full GC)觸發機制
-
指發生在老年代的GC,對象從老年代消失時,我們說 “Major Gc” 或 “Full GC” 發生了
-
出現了MajorGc,經常會伴隨至少一次的Minor GC
-
但非絕對的,在Parallel Scavenge收集器的收集策略里就有直接進行MajorGC的策略選擇過程
-
也就是在老年代空間不足時,會先嘗試觸發Minor GC,如果之后空間還不足,則觸發Major GC
-
-
Major GC的速度一般會比Minor GC慢10倍以上,STW的時間更長,如果Major GC后,內存還不足,就報OOM了
Full GC 觸發機制
觸發Full GC執行的情況有如下五種:
-
調用System.gc()時,系統建議執行Fu11GC,但是不必然執行
-
老年代空間不足
-
方法區空間不足
-
通過Minor GC后進入老年代的平均大小大於老年代的可用內存
-
由Eden區、survivor spacee(From Space)區向survivor space1(To Space)區復制時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
說明:Full GC 是開發或調優中盡量要避免的。這樣STW時間會短一些
5.4、GC 日志分析
- 代碼
1 /** 2 * 測試MinorGC 、 MajorGC、FullGC 3 * -Xms9m -Xmx9m -XX:+PrintGCDetails 4 * 5 */ 6 public class GCTest { 7 public static void main(String[] args) { 8 int i = 0; 9 try { 10 List<String> list = new ArrayList<>(); 11 String a = "test.com"; 12 while (true) { 13 list.add(a); 14 a = a + a; 15 i++; 16 } 17 18 } catch (Throwable t) { 19 t.printStackTrace(); 20 System.out.println("遍歷次數為:" + i); 21 } 22 } 23 }
- JVM 參數(PrintGCDetails打印GC詳情日志)
-Xms9m -Xmx9m -XX:+PrintGCDetails
- GC 日志:在 OOM 之前,一定會觸發一次 Full GC ,因為只有在老年代空間不足時候,才會爆出OOM異常
1 [GC (Allocation Failure) [PSYoungGen: 2048K->480K(2560K)] 2048K->640K(9728K), 0.0023805 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 2 [GC (Allocation Failure) [PSYoungGen: 2235K->496K(2560K)] 2395K->1460K(9728K), 0.0020698 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 3 [GC (Allocation Failure) [PSYoungGen: 1588K->256K(2560K)] 7672K->7108K(9728K), 0.0012447 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 4 [Full GC (Ergonomics) [PSYoungGen: 256K->0K(2560K)] [ParOldGen: 6852K->4494K(7168K)] 7108K->4494K(9728K), [Metaspace: 3287K->3287K(1056768K)], 0.0057478 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 5 [GC (Allocation Failure) [PSYoungGen: 80K->32K(2560K)] 6622K->6574K(9728K), 0.0007642 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 6 [Full GC (Ergonomics) [PSYoungGen: 32K->0K(2560K)] [ParOldGen: 6542K->4488K(7168K)] 6574K->4488K(9728K), [Metaspace: 3287K->3287K(1056768K)], 0.0034269 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 7 [GC (Allocation Failure) [PSYoungGen: 49K->64K(2560K)] 6585K->6600K(9728K), 0.0007259 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 8 [Full GC (Ergonomics) [PSYoungGen: 64K->0K(2560K)] [ParOldGen: 6536K->6536K(7168K)] 6600K->6536K(9728K), [Metaspace: 3295K->3295K(1056768K)], 0.0056462 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 9 [GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 6536K->6536K(8704K), 0.0004218 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 10 [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] [ParOldGen: 6536K->6518K(7168K)] 6536K->6518K(8704K), [Metaspace: 3295K->3295K(1056768K)], 0.0048128 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 11 遍歷次數為:17 12 Heap 13 PSYoungGen total 1536K, used 79K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000) 14 eden space 1024K, 7% used [0x00000007bfd00000,0x00000007bfd13c30,0x00000007bfe00000) 15 from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000) 16 to space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000) 17 ParOldGen total 7168K, used 6518K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000) 18 object space 7168K, 90% used [0x00000007bf600000,0x00000007bfc5da28,0x00000007bfd00000) 19 Metaspace used 3332K, capacity 4496K, committed 4864K, reserved 1056768K 20 class space used 368K, capacity 388K, committed 512K, reserved 1048576K 21 java.lang.OutOfMemoryError: Java heap space 22 at java.util.Arrays.copyOf(Arrays.java:3332) 23 at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124) 24 at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448) 25 at java.lang.StringBuilder.append(StringBuilder.java:136) 26 at com.test.jvm2.GCTest.main(GCTest.java:19) 27 28 Process finished with exit code 0
-
分析第四行Full GC
-
[PSYoungGen: 256K->0K(2560K)]:年輕代總空間為 2560K ,當前占用 256K ,經過垃圾回收后剩余 0K
-
[ParOldGen: 6852K->4494K(7168K)]:老年代總空間為 7168K ,當前占用 6852K ,經過垃圾回收后剩余 4494K
-
7108K->4494K(9728K):堆內存總空間為 9728K ,當前占用 7108K ,經過垃圾回收后剩余 4494K
-
[Metaspace: 3287K->3287K(1056768K)]:元空間總空間為 1056768K ,當前占用 3452K ,經過垃圾回收后剩余 3452K
-
0.0057478 secs :垃圾回收用時 0.0057478 secs
-
六、堆空間分代思想
為什么需要分代?
-
為什么要把Java堆分代?不分代就不能正常工作了嗎?經研究,不同對象的生命周期不同。70%-99%的對象是臨時對象。
-
新生代:有Eden、兩塊大小相同的survivor(又稱為from/to,s0/s1)構成,to總為空。
-
老年代:存放新生代中經歷多次GC仍然存活的對象。
-
-
其實不分代完全可以,分代的唯一理由就是優化GC性能。
-
如果沒有分代,那所有的對象都在一塊,就如同把一個學校的人都關在一個教室。GC的時候要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描。
-
而很多對象都是朝生夕死的,如果分代的話,把新創建的對象放到某一地方,當GC的時候先把這塊存儲“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。
-
JDK7 與 JDK8內存划分區別
七、內存分配策略
內存分配策略或對象提升(Promotion)規則
-
如果對象在Eden出生並經過第一次Minor GC后仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並將對象年齡設為1。
-
對象在Survivor區中每熬過一次MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲,其實每個JVM、每個GC都有所不同)時,就會被晉升到老年代
-
對象晉升老年代的年齡閥值,可以通過選項**-XX:MaxTenuringThreshold**來設置
針對不同年齡段的對象分配原則如下所示:
-
優先分配到Eden:開發中比較長的字符串或者數組,會直接存在老年代,但是因為新創建的對象都是朝生夕死的,所以這個大對象可能也很快被回收,但是因為老年代觸發Major GC的次數比 Minor GC要更少,因此可能回收起來就會比較慢
-
大對象直接分配到老年代:盡量避免程序中出現過多的大對象
-
長期存活的對象分配到老年代
-
動態對象年齡判斷:如果Survivor區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
-
空間分配擔保: -XX:HandlePromotionFailure ,也就是經過Minor GC后,所有的對象都存活,因為Survivor比較小,所以就需要將Survivor無法容納的對象,存放到老年代中。
代碼示例
- 代碼
1 /** 2 * 測試:大對象直接進入老年代 3 * -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails 4 * 5 * 參數設置后: 6 * 新生代--20m 7 * Eden--16m S0--2m S1--2m 8 * 老年代--40m 9 */ 10 public class YoungOldAreaTest { 11 public static void main(String[] args) { 12 byte[] buffer = new byte[1024 * 1024 * 20];//20m 13 14 } 15 }
- JVM 參數
-Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
- 整個過程並沒有進行垃圾回收,並且 ParOldGen 區直接占用了 20MB 的空間,說明大對象直接懟到了老年代中
八、為對象分配內存
8.1、什么是 TLAB(Thread Local Allocation Buffer)
-
從內存模型而不是垃圾收集的角度,對Eden區域繼續進行划分,JVM為每個線程分配了一個私有緩存區域,它包含在Eden空間內。
-
多線程同時分配內存時,使用TLAB可以避免一系列的非線程安全問題,同時還能夠提升內存分配的吞吐量,因此我們可以將這種內存分配方式稱之為快速分配策略。
-
據我所知所有OpenJDK衍生出來的JVM都提供了TLAB的設計。
8.2、為什么有 TLAB(Thread Local Allocation Buffer)
思考問題:堆空間都是共享的么?
不一定,因為還有TLAB這個概念,在堆中划分出一塊區域,為每個線程所獨占
為什么有TLAB?
堆區是線程共享區域,任何線程都可以訪問到堆區中的共享數據,由於對象實例的創建在JVM中非常頻繁,因此在並發環境下從堆區中划分內存空間是線程不安全的,為避免多個線程操作同一地址,即在多線程創建對象時,都需要從堆空間申請內存空間來存放對象,那么需要使用加鎖等機制,來確保多線程不是使用同一地址來創建對象,而加鎖進而影響分配速度,以及程序運行速度。
8.3、TLAB 分配過程
-
盡管不是所有的對象實例都能夠在TLAB中成功分配內存,但JVM確實是將TLAB作為內存分配的首選。
-
在程序中,開發人員可以通過選項“-XX:UseTLAB”設置是否開啟TLAB空間,默認開啟。
-
默認情況下,TLAB空間的內存非常小,僅占有整個Eden空間的1%,當然我們可以通過選項“-XX:TLABWasteTargetPercent”設置TLAB空間所占用Eden空間的百分比大小。
-
一旦對象在TLAB空間分配內存失敗時,JVM就會嘗試着通過使用加鎖機制確保數據操作的原子性,從而直接在Eden空間中分配內存。
TLAB 分配過程圖
代碼示例
-
代碼
1 /** 2 * 測試-XX:UseTLAB參數是否開啟的情況:默認情況是開啟的 3 * 4 */ 5 public class TLABArgsTest { 6 public static void main(String[] args) { 7 System.out.println("我只是來打個醬油~"); 8 try { 9 Thread.sleep(1000000); 10 } catch (InterruptedException e) { 11 e.printStackTrace(); 12 } 13 } 14 }
-
查看 UseTLAB 標志位的狀態
命令:jps
命令:jinfo -flag UseTLAB 進程id
-
沒有設置任何 JVM 參數,通過命令行查看 TLAB 是否開啟:結論是默認情況是開啟 TLAB
九、堆空間參數設置
9.1、常用參數設置
官方文檔:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
常用參數設置
-
-XX:+PrintFlagsInitial:查看所有的參數的默認初始值
-
-XX:+PrintFlagsFinal:查看所有的參數的最終值(可能會存在修改,不再是初始值)
-
-Xms:初始堆空間內存(默認為物理內存的1/64)
-
-Xmx:最大堆空間內存(默認為物理內存的1/4)
-
-Xmn:設置新生代的大小(初始值及最大值)
-
-XX:NewRatio:配置新生代與老年代在堆結構的占比
-
-XX:SurvivorRatio:設置新生代中Eden和S0/S1空間的比例
-
-XX:MaxTenuringThreshold:設置新生代垃圾的最大年齡
-
-XX:+PrintGCDetails:輸出詳細的GC處理日志
-
-XX:+PrintGC 或 -verbose:gc :打印gc簡要信息
-
-XX:HandlePromotionFalilure:是否設置空間分配擔保
9.2、空間分配擔保
在發生Minor GC之前,虛擬機會檢查老年代最大可用的連續空間是否大於新生代所有對象的總空間。
-
如果大於,則此次Minor GC是安全的
-
如果小於,則虛擬機會查看**-XX:HandlePromotionFailure**設置值是否允擔保失敗。
-
如果HandlePromotionFailure=true,那么會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的對象的平均大小。
-
如果大於,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的;
-
如果小於,則進行一次Full GC。
-
-
如果HandlePromotionFailure=false,則進行一次Full GC。
-
9.3、代碼示例

1 /** 2 * 測試堆空間常用的jvm參數: 3 * -XX:+PrintFlagsInitial : 查看所有的參數的默認初始值 4 * -XX:+PrintFlagsFinal :查看所有的參數的最終值(可能會存在修改,不再是初始值) 5 * 具體查看某個參數的指令: 6 * jps:查看當前運行中的進程 7 * jinfo -flag SurvivorRatio 進程id 8 * -Xms:初始堆空間內存 (默認為物理內存的1/64) 9 * -Xmx:最大堆空間內存(默認為物理內存的1/4) 10 * -Xmn:設置新生代的大小。(初始值及最大值) 11 * -XX:NewRatio:配置新生代與老年代在堆結構的占比 12 * -XX:SurvivorRatio:設置新生代中Eden和S0/S1空間的比例 13 * -XX:MaxTenuringThreshold:設置新生代垃圾的最大年齡 14 * -XX:+PrintGCDetails:輸出詳細的GC處理日志 15 * 打印gc簡要信息:① -XX:+PrintGC ② -verbose:gc 16 * -XX:HandlePromotionFailure:是否設置空間分配擔保 17 * 18 */ 19 public class HeapArgsTest { 20 public static void main(String[] args) { 21 22 } 23 }
十、逃逸分析
面試題:堆是分配對象的唯一選擇么?
在《深入理解Java虛擬機》中關於Java堆內存有這樣一段描述:
-
隨着JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那么“絕對”了。
-
在Java虛擬機中,對象是在Java堆中分配內存的,這是一個普遍的常識。但是,有一種特殊情況,那就是如果經過逃逸分析(Escape Analysis)后發現,一個對象並沒有逃逸出方法的話,那么就可能被優化成棧上分配。這樣就無需在堆上分配內存,也無須進行垃圾回收了。這也是最常見的堆外存儲技術。
-
此外,前面提到的基於OpenJDK深度定制的TaoBao VM,其中創新的GCIH(GC invisible heap)技術實現off-heap,將生命周期較長的Java對象從heap中移至heap外,並且GC不能管理GCIH內部的Java對象,以此達到降低GC的回收頻率和提升GC的回收效率的目的
如何將堆上的對象分配到棧,需要使用逃逸分析手段。
逃逸分析介紹
-
這是一種可以有效減少Java程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。
-
通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上。
-
逃逸分析的基本行為就是分析對象動態作用域:
-
當一個對象在方法中被定義后,對象只在方法內部使用,則認為沒有發生逃逸。
-
當一個對象在方法中被定義后,它被外部方法所引用,則認為發生逃逸。例如作為調用參數傳遞到其他地方中。
-
10.1、逃逸分析
- 逃逸分析的舉例
1 /** 2 * 逃逸分析 3 * 4 * 如何快速的判斷是否發生了逃逸分析,大家就看new的對象實體是否有可能在方法外被調用。 5 */ 6 public class EscapeAnalysis { 7 8 public EscapeAnalysis obj; 9 10 /* 11 方法返回EscapeAnalysis對象,發生逃逸 12 */ 13 public EscapeAnalysis getInstance(){ 14 return obj == null? new EscapeAnalysis() : obj; 15 } 16 17 /* 18 為成員屬性賦值,發生逃逸 19 */ 20 public void setObj(){ 21 this.obj = new EscapeAnalysis(); 22 } 23 //思考:如果當前的obj引用聲明為static的?仍然會發生逃逸。 24 25 /* 26 對象的作用域僅在當前方法中有效,沒有發生逃逸 27 */ 28 29 public void useEscapeAnalysis(){ 30 EscapeAnalysis e = new EscapeAnalysis(); 31 } 32 33 /* 34 引用成員變量的值,發生逃逸 35 */ 36 public void useEscapeAnalysis1(){ 37 EscapeAnalysis e = getInstance(); 38 //getInstance().xxx()同樣會發生逃逸 39 } 40 }
-
逃逸分析參數設置
-
在JDK 1.7 版本之后,HotSpot中默認就已經開啟了逃逸分析
-
如果使用的是較早的版本,開發人員則可以通過:
-
選項“-XX:+DoEscapeAnalysis"顯式 ,DoEscapeAnalysis默認開啟
-
通過選項“-XX:+PrintEscapeAnalysis"查看逃逸分析的篩選結果
-
-
逃逸分析結論
開發中能使用局部變量的,就不要使用在方法外定義。
-
逃逸分析之代碼優化
使用逃逸分析,編譯器可以對代碼做如下優化:
-
棧上分配:將堆分配轉化為棧分配。如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會發生逃逸,對象可能是棧上分配的候選,而不是堆上分配
-
同步省略:如果一個對象被發現只有一個線程被訪問到,那么對於這個對象的操作可以不考慮同步。
-
分離對象或標量替換:有的對象可能不需要作為一個連續的內存結構存在也可以被訪問到,那么對象的部分(或全部)可以不存儲在內存,而是存儲在CPU寄存器中。
10.2、棧上分配
棧上分配
-
JIT編譯器在編譯期間根據逃逸分析的結果,發現如果一個對象並沒有逃逸出方法的話,就可能被優化成棧上分配。
-
分配完成后,繼續在調用棧內執行,最后線程結束,棧空間被回收,局部變量對象也被回收。這樣就無須進行垃圾回收了。
-
常見的棧上分配的場景:在逃逸分析中,已經說明了,分別是給成員變量賦值、方法返回值、實例引用傳遞。
棧上分配舉例
- 代碼
1 /** 2 * 棧上分配測試 3 * 參數:-Xmx256m -Xms256m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails 4 * 5 * DoEscapeAnalysis默認開啟 6 * -XX:-DoEscapeAnalysis:關閉逃逸分析 7 */ 8 public class StackAllocation { 9 public static void main(String[] args) { 10 long start = System.currentTimeMillis(); 11 12 for (int i = 0; i < 10000000; i++) { 13 alloc(); 14 } 15 // 查看執行時間 16 long end = System.currentTimeMillis(); 17 System.out.println("花費的時間為: " + (end - start) + " ms"); 18 // 為了方便查看堆內存中對象個數,線程sleep 19 try { 20 Thread.sleep(1000000); 21 } catch (InterruptedException e1) { 22 e1.printStackTrace(); 23 } 24 } 25 26 private static void alloc() { 27 User user = new User();//未發生逃逸 28 } 29 30 static class User { 31 32 } 33 }
-
未開啟逃逸分析的情況
-
JVM 參數設置:-Xmx256m -Xms256m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
-
運行程序,日志打印:發生了 GC ,耗時 72ms
- 堆上面有好多好多 User 對象
-
-
開啟逃逸分析的情況
-
JVM 參數設置:-Xmx256m -Xms256m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
-
運行程序,日志打印:並沒有發生 GC ,耗時 8ms ,棧上分配是真的快啊
-
總結
通過情況1:未開啟逃逸,有GC日志打印,且對中User對象很多,情況2:開啟逃逸,無GC日志,且堆中對象少;說明部分對象不在堆中創建的,那么間接說明了棧上分配
Java對象分配流程總結
10.3、同步省略
同步省略
-
線程同步的代價是相當高的,同步的后果是降低並發性和性能。
-
在動態編譯同步塊的時候,JIT編譯器可以借助逃逸分析來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問而沒有被發布到其他線程。
-
如果沒有,那么JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提高並發性和性能。這個取消同步的過程就叫同步省略,也叫鎖消除。
同步省略代碼舉例
- 代碼1
1 public void f() { 2 Object o = new Object(); 3 synchronized(o) { 4 System.out.println(o); 5 } 6 }
-
代碼中對o這個對象加鎖,但是o對象的生命周期只在f()方法中,並不會被其他線程所訪問到,所以在JIT編譯階段就會被優化掉,優化成代碼2:
1 public void f() { 2 Object o = new Object(); 3 System.out.println(o); 4 }
-
字節碼分析代碼1
- 注意:字節碼文件中並沒有進行優化,可以看到加鎖和釋放鎖的操作依然存在,同步省略操作是在解釋運行時發生的
10.4、標量替換
分離對象或標量替換
-
標量(scalar)是指一個無法再分解成更小的數據的數據。Java中的原始數據類型就是標量。
-
相對的,那些還可以分解的數據叫做聚合量(Aggregate),Java中的對象就是聚合量,因為他可以分解成其他聚合量和標量。
-
在JIT階段,如果經過逃逸分析,發現一個對象不會被外界訪問的話,那么經過JIT優化,就會把這個對象拆解成若干個其中包含的若干個成員變量來代替。這個過程就是標量替換。
標量替換舉例
- 代碼
1 public static void main(String args[]) { 2 alloc(); 3 } 4 class Point { 5 private int x; 6 private int y; 7 } 8 private static void alloc() { 9 Point point = new Point(1,2); 10 System.out.println("point.x" + point.x + ";point.y" + point.y); 11 }
- 以上代碼,經過標量替換后,就會變成
1 private static void alloc() { 2 int x = 1; 3 int y = 2; 4 System.out.println("point.x = " + x + "; point.y=" + y); 5 }
結論:
-
可以看到,Point這個聚合量經過逃逸分析后,發現他並沒有逃逸,就被替換成兩個聚合量了。
-
那么標量替換有什么好處呢?就是可以大大減少堆內存的占用。因為一旦不需要創建對象了,那么就不再需要分配堆內存了。
-
標量替換為棧上分配提供了很好的基礎。
標量替換參數設置
參數 -XX:+ElimilnateAllocations:開啟了標量替換(默認打開),允許將對象打散分配在棧上。
代碼示例
-
代碼
1 /** 2 * 標量替換測試 3 * -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations 4 * 5 */ 6 public class ScalarReplace { 7 public static class User { 8 public int id; 9 public String name; 10 } 11 12 public static void alloc() { 13 User u = new User();//未發生逃逸 14 u.id = 5; 15 u.name = "www.test.com"; 16 } 17 18 public static void main(String[] args) { 19 long start = System.currentTimeMillis(); 20 for (int i = 0; i < 10000000; i++) { 21 alloc(); 22 } 23 long end = System.currentTimeMillis(); 24 System.out.println("花費的時間為: " + (end - start) + " ms"); 25 } 26 }
-
未開啟標量替換
-
JVM 參數:-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
-
日志分析:伴隨着 GC 的垃圾回收,用時 112ms
-
-
開啟標量替換
-
JVM 參數:-Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
-
日志分析:無垃圾回收,用時 9ms
-
逃逸分析參數設置總結
-
上述代碼在主函數中調用了1億次alloc()方法,進行對象創建
-
由於User對象實例需要占據約16字節的空間,因此累計分配空間達到將近1.5GB。
-
如果堆空間小於這個值,就必然會發生GC。使用如下參數運行上述代碼:
- -server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
這里設置參數如下:
-
參數 -server:啟動Server模式,因為在server模式下,才可以啟用逃逸分析。
-
參數 -XX:+DoEscapeAnalysis:啟用逃逸分析
-
參數 -Xmx10m:指定了堆空間最大為10MB
-
參數 -XX:+PrintGC:將打印GC日志。
-
參數 -XX:+EliminateAllocations:開啟了標量替換(默認打開),允許將對象打散分配在棧上,比如對象擁有id和name兩個字段,那么這兩個字段將會被視為兩個獨立的局部變量進行分配
10.5、逃逸分析的不足
-
關於逃逸分析的論文在1999年就已經發表了,但直到JDK1.6才有實現,而且這項技術到如今也並不是十分成熟的。
-
其根本原因就是無法保證逃逸分析的性能消耗一定能高於他的消耗。雖然經過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列復雜的分析的,這其實也是一個相對耗時的過程。一個極端的例子,就是經過逃逸分析之后,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。
-
雖然這項技術並不十分成熟,但是它也是即時編譯器優化技術中一個十分重要的手段。注意到有一些觀點,認為通過逃逸分析,JVM會在棧上分配那些不會逃逸的對象,這在理論上是可行的,但是取決於JVM設計者的選擇。
-
據了解,Oracle Hotspot JVM中並未這么做,這一點在逃逸分析相關的文檔里已經說明,所以可以明確所有的對象實例都是創建在堆上。
-
目前很多書籍還是基於JDK7以前的版本,JDK已經發生了很大變化,intern字符串的緩存和靜態變量曾經都被分配在永久代上,而永久代已經被元數據區取代。但是intern字符串緩存和靜態變量並不是被轉移到元數據區,而是直接在堆上分配,所以這一點同樣符合前面一點的結論:對象實例都是分配在堆上。
十一、堆小結
-
年輕代是對象的誕生、成長、消亡的區域,一個對象在這里產生、應用,最后被垃圾回收器收集、結束生命。
-
老年代放置長生命周期的對象,通常都是從Survivor區域篩選拷貝過來的Java對象。
-
當然,也有特殊情況,我們知道普通的對象可能會被分配在TLAB上;
-
如果對象較大,無法分配在 TLAB 上,則JVM會試圖直接分配在Eden其他位置上;
-
如果對象太大,完全無法在新生代找到足夠長的連續空閑空間,JVM就會直接分配到老年代。
-
當GC只發生在年輕代中,回收年輕代對象的行為被稱為Minor GC。
-
當GC發生在老年代時則被稱為Major GC或者Full GC。
-
一般的,Minor GC的發生頻率要比Major GC高很多,即老年代中垃圾回收發生的頻率將大大低於年輕代。