运行数据区②---堆
本篇目錄
- 1. 核心概述
- 1.1 配置jvm及查看jvm進程
- 1.2 分析SimpleHeap的jvm情況
- 1.3 堆的細分內存結構
- JDK 7以前: 新生區+老年區+永久區
- JDK 8以后: 新生區+老年區+元空間
- 2.設置堆內存大小與OOM
- 2.1 查看堆內存大小
- 2.2 堆大小分析
- 2.3 OOM
- 3.年輕代與老年代
- 4.圖解對象分配過程
- 4.1 一般分配過程
- 4.2 對象分配的特殊情況
- 4.3 常用調優工具
- 5.Minor GC、Major GC、Full GC
- 5.1 年輕代GC(Minor GC)觸發機制:
- 5.2 老年代GC(Major GC/Full GC)觸發機制
- 5.3 Full GC觸發機制
- 6.堆空間分代思想
- 7.內存分配策略
- 8.為對象分配內存:TLAB(線程私有緩存區域)
- 8.1 為什么有TLAB(Thread Local Allocation Buffer)
- 8.2 什么是TLAB
- 8.3 TLAB對象分配過程
- 9. 堆空間的參數設置
- 10.堆是分配對象的唯一選擇么
- 10.1 代碼分析
- 10.2 逃逸分析
- 10.3 參數設置
- 10.4 代碼優化
- ① 棧上分配
- ② 同步省略
- ③ 分離對象或標量替換
- 10.5 逃逸分析小結
1. 核心概述
一個進程對應一個jvm實例,一個運行時數據區,又包含多個線程,這些線程共享了方法區和堆,每個線程包含了程序計數器、本地方法棧和虛擬機棧。
? 從實際使用的角度看,“幾乎”所有的對象的實例都在這里分配內存 (‘幾乎’是因為可能存儲在棧上)
1.1 配置jvm及查看jvm進程
- 編寫HeapDemo/HeapDemo1代碼
- 首先對虛擬機進行配置,如圖 Run-Edit configurations
- 在jdk目錄,我的是…/jdk1.8.0_171.jdk/Contents/Home/bin下找到jvisualvm 運行(或者直接終端運行jvisualvm),查看進程,可以看到我們設置的配置信息
1.2 分析SimpleHeap的jvm情況
public class SimpleHeap {private int id;//屬性、成員變量public SimpleHeap(int id) {this.id = id;}public void show() {System.out.println("My ID is " + id);}public static void main(String[] args) {SimpleHeap sl = new SimpleHeap(1);SimpleHeap s2 = new SimpleHeap(2);int[] arr = new int[10];Object[] arr1 = new Object[10];} }1.3 堆的細分內存結構
JDK 7以前: 新生區+老年區+永久區
- Young Generation Space:又被分為Eden區和Survior區 Young/New
- Tenure generation Space: Old/Tenure
- Permanent Space: Perm
JDK 8以后: 新生區+老年區+元空間
- Young Generation Space:又被分為Eden區和Survior區 Young/New
- Tenure generation Space: Old/Tenure
- Meta Space: Meta
2.設置堆內存大小與OOM
? Java堆區用于存儲java對象實例,堆的大小在jvm啟動時就已經設定好了,可以通過 "-Xmx"和 "-Xms"來進行設置
- -Xms 用于表示堆的起始內存,等價于 -XX:InitialHeapSize
-
-Xms 用來設置堆空間(年輕代+老年代)的初始內存大小
- -X 是jvm的運行參數
- ms 是memory start
-
-Xmx 用于設置堆的最大內存,等價于 -XX:MaxHeapSize
-
? 一旦堆區中的內存大小超過 -Xmx所指定的最大內存時,將會拋出OOM異常
? 通常會將-Xms和-Xmx兩個參數配置相同的值,其目的就是為了能夠在java垃圾回收機制清理完堆區后不需要重新分隔計算堆區的大小,從而提高性能
? 默認情況下,初始內存大小:物理內存大小/64;最大內存大小:物理內存大小/4
- 手動設置:-Xms600m -Xmx600m
? 查看設置的參數:
- 方式一: 終端輸入jps , 然后 jstat -gc 進程id
- 方式二:(控制臺打印)Edit Configurations->VM Options 添加 -XX:+PrintGCDetails
2.1 查看堆內存大小
public class HeapSpaceInitial {public static void main(String[] args) {//返回Java虛擬機中的堆內存總量long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;//返回Java虛擬機試圖使用的最大堆內存量long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;System.out.println("-Xms : " + initialMemory + "M");//-Xms : 575MSystem.out.println("-Xmx : " + maxMemory + "M");//-Xmx : 575MSystem.out.println("系統內存大小為:" + initialMemory * 64.0 / 1024 + "G");//系統內存大小為:35.9375GSystem.out.println("系統內存大小為:" + maxMemory * 4.0 / 1024 + "G");//系統內存大小為:2.24609375Gtry {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}} }2.2 堆大小分析
設置堆大小為600m,打印出的結果為575m,這是因為幸存者區S0和S1各占據了25m,但是他們始終有一個是空的,存放對象的是伊甸園區和一個幸存者區
C:\Users\Administrator>jps 1568 17344 Jps 17732 Launcher 2020 HeapSpaceInitial 8824 MainC:\Users\Administrator>jstat -gc 2020S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 25600.0 25600.0 0.0 0.0 153600.0 15360.2 409600.0 0.0 4480.0 675.0 384.0 66.6 0 0.000 0 0.000 0.000C:\Users\Administrator>2.3 OOM
java.lang.OutOfMemoryError: Java heap space
/*** -Xms600m -Xmx600m*/ public class OOMTest {public static void main(String[] args) {ArrayList<Picture> list = new ArrayList<>();while(true){try {Thread.sleep(20);} catch (InterruptedException e) {e.printStackTrace();}list.add(new Picture(new Random().nextInt(1024 * 1024)));}} } class Picture{private byte[] pixels;public Picture(int length) {this.pixels = new byte[length];} }//結果輸出: Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat com.oujiong.jvm.Picture.<init>(OOMTest.java:33)at com.oujiong.jvm.OOMTest.main(OOMTest.java:26)Process finished with exit code 13.年輕代與老年代
? 存儲在JVM中的java對象可以被劃分為兩類:
? Java堆區進一步細分可以分為年輕代(YoungGen)和老年代(OldGen)
? 其中年輕代可以分為Eden空間、Survivor0空間和Survivor1空間(有時也叫frmo區,to區)
? 配置新生代與老年代在堆結構的占比
-
在hotSpot中,Eden空間和另外兩個Survivor空間缺省所占的比例是8:1:1,開發人員可以通過選項 -XX:SurvivorRatio 調整空間比例,如-XX:SurvivorRatio=8
-
幾乎所有的Java對象都是在Eden區被new出來的
-
絕大部分的Java對象都銷毀在新生代了(IBM公司的專門研究表明,新生代80%的對象都是“朝生夕死”的)
-
可以使用選項-Xmn設置新生代最大內存大小(這個參數一般使用默認值就好了)
測試代碼:
/*** -Xms600m -Xmx600m** -XX:NewRatio : 設置新生代與老年代的比例。默認值是2.* -XX:SurvivorRatio :設置新生代中Eden區與Survivor區的比例。默認值是8* -XX:-UseAdaptiveSizePolicy :關閉自適應的內存分配策略 '-'關閉,'+'打開 (暫時用不到)* -Xmn:設置新生代的空間的大小。 (一般不設置)**/ public class EdenSurvivorTest {public static void main(String[] args) {System.out.println("我只是來打個醬油~");try {Thread.sleep(1000000);} catch (InterruptedException e) {e.printStackTrace();}} }4.圖解對象分配過程
4.1 一般分配過程
為新對象分配內存是件非常嚴謹和復雜的任務,JVM的設計者們不僅需要考慮內存如何分配、在哪里分配的問題,并且由于內存分配算法與內存回收算法密切相關,所以還需要考慮GC執行完內存回收后是否會在內存空間中產生內存碎片。
總結
針對幸存者s0,s1區:復制之后有交換,誰空誰是to
關于垃圾回收:頻繁在新生區收集,很少在老年區收集,幾乎不在永久區/元空間收集。
4.2 對象分配的特殊情況
4.3 常用調優工具
? JDK命令行
? Eclipse:Memory Analyzer Tool
? Jconsole
? VisualVM
? Jprofiler
? Java Flight Recorder
? GCViewer
? GC Easy
5.Minor GC、Major GC、Full GC
JVM在進行GC時,并非每次都針對上面三個內存區域(新生代、老年代、方法區)一起回收的,大部分時候回收都是指新生代。
針對hotSpot VM的實現,它里面的GC按照回收區域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)
-
新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
-
老年代收集(Major GC/Old GC):只是老年代的垃圾收集
---- 目前,只有CMS GC會有單獨收集老年代的行為。注意,很多時候Major GC 會和 Full GC混淆使用,需要具體分辨是老年代回收還是整堆回收
-
混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集
----目前,只有G1 GC會有這種行為
5.1 年輕代GC(Minor GC)觸發機制:
? 當年輕代空間不足時,就會觸發Minor GC,這里的年輕代滿指的是Eden代滿,Survivor滿不會引發GC.(每次Minor GC會清理年輕代的內存,Survivor是被動GC,不會主動GC)
? 因為Java對象大多都具備朝生夕滅的特性,所以Minor GC 非常頻繁,一般回收速度也比較快,這一定義既清晰又利于理解。
? Minor GC 會引發STW(Stop the World),暫停其他用戶的線程,等垃圾回收結束,用戶線程才恢復運行。
5.2 老年代GC(Major GC/Full GC)觸發機制
? 指發生在老年代的GC,對象從老年代消失時,Major GC 或者 Full GC 發生了
? 出現了Major GC,經常會伴隨至少一次的Minor GC(不是絕對的,在Parallel Scavenge 收集器的收集策略里就有直接進行Major GC的策略選擇過程)
- 也就是老年代空間不足時,會先嘗試觸發Minor GC。如果之后空間還不足,則觸發Major GC
? Major GC速度一般會比Minor GC慢10倍以上,STW時間更長
? 如果Major GC后,內存還不足,就報OOM了
5.3 Full GC觸發機制
- 觸發Full GC執行的情況有以下五種:
①調用System.gc()時,系統建議執行Full GC,但是不必然執行
②老年代空間不足
③方法區空間不足
④通過Minor GC后進入老年代的平均大小小于老年代的可用內存
⑤由Eden區,Survivor S0(from)區向S1(to)區復制時,對象大小小于To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小于該對象大小 - 說明:Full GC 是開發或調優中盡量要避免的,這樣暫停時間會短一些
代碼演示:
Young GC ->Full GC -> OOM
輸出結果:
[GC (Allocation Failure) [PSYoungGen: 2048K->507K(2560K)] 2048K->994K(9728K), 0.0023094 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 2457K->504K(2560K)] 2944K->1630K(9728K), 0.0022268 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 2113K->491K(2560K)] 3240K->2994K(9728K), 0.0018447 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 2083K->491K(2560K)] 4586K->4546K(9728K), 0.0019428 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 1329K->0K(2560K)] [ParOldGen: 7126K->4169K(7168K)] 8456K->4169K(9728K), [Metaspace: 3347K->3347K(1056768K)], 0.0106081 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC (Allocation Failure) --[PSYoungGen: 1562K->1562K(2560K)] 5731K->7267K(9728K), 0.0013447 secs] [Times: user=0.02 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 1562K->0K(2560K)] [ParOldGen: 5705K->5705K(7168K)] 7267K->5705K(9728K), [Metaspace: 3352K->3352K(1056768K)], 0.0039439 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 5705K->5705K(9728K), 0.0005427 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 5705K->5595K(7168K)] 5705K->5595K(9728K), [Metaspace: 3352K->3352K(1056768K)], 0.0165113 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] java.lang.OutOfMemoryError: Java heap spaceat java.util.Arrays.copyOf(Arrays.java:3332)at java.lang.AbstractStringBuilder.expandCapacity(AbstractStringBuilder.java:137)at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:121)at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:421)at java.lang.StringBuilder.append(StringBuilder.java:136)at com.oujiong.jvm.GCTest.main(GCTest.java:26) 遍歷次數為:17 HeapPSYoungGen total 2560K, used 87K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)eden space 2048K, 4% used [0x00000000ffd00000,0x00000000ffd15e80,0x00000000fff00000)from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)ParOldGen total 7168K, used 5595K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)object space 7168K, 78% used [0x00000000ff600000,0x00000000ffb76f90,0x00000000ffd00000)Metaspace used 3384K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 363K, capacity 388K, committed 512K, reserved 1048576KProcess finished with exit code 06.堆空間分代思想
為什么要把Java堆分代?不分代就不能正常工作了么
- 經研究,不同對象的生命周期不同。70%-99%的對象都是臨時對象。
? 新生代:有Eden、Survivor構成(s0,s1 又稱為from to),to總為空
? 老年代:存放新生代中經歷多次依然存活的對象 - 其實不分代完全可以,分代的唯一理由就是優化GC性能。如果沒有分代,那所有的對象都在一塊,就如同把一個學校的人都關在一個教室。GC的時候要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描,而很多對象都是朝生夕死的,如果分代的話,把新創建的對象放到某一地方,當GC的時候先把這塊存儲“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。
7.內存分配策略
- 如果對象在Eden出生并經過第一次Minor GC后依然存活,并且能被Survivor容納的話,將被移動到Survivor空間中,把那個將對象年齡設為1.對象在Survivor區中每熬過一次MinorGC,年齡就增加一歲,當它的年齡增加到一定程度(默認15歲,其實每個JVM、每個GC都有所不同)時,就會被晉升到老年代中
? 對象晉升老年代的年齡閾值,可以通過選項 -XX:MaxTenuringThreshold來設置 - 針對不同年齡段的對象分配原則如下:
? 優先分配到Eden
? 大對象直接分配到老年代。盡量避免程序中出現過多的大對象
? 長期存活的對象分配到老年代
? 動態對象年齡判斷: 如果Survivor區中相同年齡的所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象可以直接進入到老年代。無需等到MaxTenuringThreshold中要求的年齡
? 空間分配擔保: -XX: HandlePromotionFailure
代碼示例:
分配60m堆空間,新生代 20m ,Eden 16m, s0 2m, s1 2m,buffer對象20m,Eden 區無法存放buffer, 直接晉升老年代
日志輸出:
8.為對象分配內存:TLAB(線程私有緩存區域)
8.1 為什么有TLAB(Thread Local Allocation Buffer)
- 堆區是線程共享區域,任何線程都可以訪問到堆區中的共享數據
- 由于對象實例的創建在JVM中非常頻繁,在并發環境下從堆區中劃分內存空間是線程不安全的
- 為避免多個線程操作同一地址,需要使用加鎖等機制,進而影響分配速度
8.2 什么是TLAB
- 從內存模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM為每個線程分配了一個私有緩存區域,它包含在Eden空間內
- 多線程同時分配內存時,使用TLAB可以避免一系列的非線程安全問題,同時還能夠提升內存分配的吞吐量,因此我們可以將這種內存分配方式稱之為快速分配策略
- 所有OpenJDK衍生出來的JVM都提供了TLAB的設計
說明
? 盡管不是所有的對象實例都能夠在TLAB中成功分配內存,但JVM明確是將TLAB作為內存分配的首選
? 在程序中,開發人員可以通過選項“-XX:UseTLAB“ 設置是否開啟TLAB空間
? 默認情況下,TLAB空間的內存非常小,僅占有整個EDen空間的1%,當然我們可以通過選項 ”-XX:TLABWasteTargetPercent“ 設置TLAB空間所占用Eden空間的百分比大小
? 一旦對象在TLAB空間分配內存失敗時,JVM就會嘗試著通過使用加鎖機制確保數據操作的原子性,從而直接在Eden空間中分配了內存
代碼演示:
終端輸入 jps,查看TLABArgsTest進程id
jinfo -flag UseTLAB 64566(進程id),輸出-XX:+UseTLAB,證明TLAB默認是開啟的
8.3 TLAB對象分配過程
9. 堆空間的參數設置
-
-XX:PrintFlagsInitial: 查看所有參數的默認初始值
-
-XX:PrintFlagsFinal:查看所有的參數的最終值(可能會存在修改,不再是初始值)
? 具體查看某個參數的指令:
? jps:查看當前運行中的進程
? jinfo -flag SurvivorRatio 進程id: 查看新生代中Eden和S0/S1空間的比例 -
-Xms: 初始堆空間內存(默認為物理內存的1/64)
-
-Xmx: 最大堆空間內存(默認為物理內存的1/4)
-
-Xmn: 設置新生代大小(初始值及最大值)
-
-XX:NewRatio: 配置新生代與老年代在堆結構的占比
-
-XX:SurvivorRatio:設置新生代中Eden和S0/S1空間的比例
-
-XX:MaxTenuringThreshold:設置新生代垃圾的最大年齡(默認15)
-
-XX:+PrintGCDetails:輸出詳細的GC處理日志
? 打印gc簡要信息:① -XX:+PrintGC ② -verbose:gc -
-XX:HandlePromotionFailure:是否設置空間分配擔保
說明:
在發生Minor Gc之前,虛擬機會檢查老年代最大可用的連續空間是否大于新生代所有對象的總空間。
- 如果大于,則此次Minor GC是安全的
- 如果小于,則虛擬機會查看-XX:HandlePromotionFailure設置值是否允許擔保失敗。(JDK 7以后的規則HandlePromotionFailure可以認為就是true)
- 如果HandlePromotionFailure=true,那么會繼續檢查老年代最大可用連續空間是否大于歷次晉升到老年代的對象的平均大小。
- √如果大于,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的;
- √如果小于,則改為進行一次FullGC。
- √如果HandlePromotionFailure=false,則改為進行一次Full GC。
- 如果HandlePromotionFailure=true,那么會繼續檢查老年代最大可用連續空間是否大于歷次晉升到老年代的對象的平均大小。
在JDK6 Update24之后(JDK7),HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察openJDK中的源碼變化,雖然源碼中還定義了HandlePromotionFailure參數,但是在代碼中已經不會再使用它。JDK6 Update24之后的規則變為只要老年代的連續空間大于新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。
10.堆是分配對象的唯一選擇么
堆不是分配對象的唯一選擇
在《深入理解Java虛擬機》中關于Java堆內存有這樣一段描述:隨著JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那么“絕對”了。
??在Java虛擬機中,對象是在Java堆中分配內存的,這是一個普遍的常識。但是,有一種特殊情況,那就是如果經過逃逸分析(Escape Analysis)后發現,一個對象并沒有逃逸出方法的話,那么就可能被優化成棧上分配。這樣就無需在堆上分配內存,也無須進行垃圾回收了。這也是最常見的堆外存儲技術。
??此外,前面提到的基于OpenJDK深度定制的TaoBaoVM,其中創新的GCIH(GCinvisible heap)技術實現off-heap,將生命周期較長的Java對象從heap中移至heap外,并且GC不能管理GCIH內部的Java對象,以此達到降低GC的回收頻率和提升GC的回收效率的目的。
- 如何將堆上的對象分配到棧,需要使用逃逸分析手段。
- 這是一種可以有效減少Java程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。
- 通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上。
- 逃逸分析的基本行為就是分析對象動態作用域:
? 當一個對象在方法中被定義后,對象只在方法內部使用,則認為沒有發生逃逸。
? 當一個對象在方法中被定義后,它被外部方法所引用,則認為發生逃逸。例如作為調用參數傳遞到其他地方中。 - 如何快速的判斷是否發生了逃逸分析,就看new的對象實體是否有可能在方法外被調用
10.1 代碼分析
public void method(){V v = new V();//use V//......v = null; }1、沒有發生逃逸的對象,則可以分配到棧上,隨著方法執行的結束,棧空間就被移除。
public static StringBuffer createStringBuffer(String s1,String s2){StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb; }
2、由于上述方法返回的sb在方法外被使用,發生了逃逸,上述代碼如果想要StringBuffer sb不逃出方法,可以這樣寫:
public static String createStringBuffer(String s1,String s2){StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString(); }10.2 逃逸分析
/*** 逃逸分析** 如何快速的判斷是否發生了逃逸分析,就看new的對象實體是否有可能在方法外被調用。*/ public class EscapeAnalysis {public EscapeAnalysis obj;/*方法返回EscapeAnalysis對象,發生逃逸*/public EscapeAnalysis getInstance(){return obj == null? new EscapeAnalysis() : obj;}/*為成員屬性賦值,發生逃逸*/public void setObj(){this.obj = new EscapeAnalysis();}//思考:如果當前的obj引用聲明為static的?仍然會發生逃逸。/*對象的作用域僅在當前方法中有效,沒有發生逃逸*/public void useEscapeAnalysis(){EscapeAnalysis e = new EscapeAnalysis();}/*引用成員變量的值,發生逃逸*/public void useEscapeAnalysis1(){EscapeAnalysis e = getInstance();//getInstance().xxx()同樣會發生逃逸} }10.3 參數設置
- 在JDK 6u23版本之后,HotSpot中默認就已經開啟了逃逸分析
- 如果使用了較早的版本,開發人員可以通過
? -XX:DoEscapeAnalysis 顯式開啟逃逸分析
? -XX:+PrintEscapeAnalysis查看逃逸分析的篩選結果
結論
開發中能使用局部變量的,就不要使用在方法外定義
10.4 代碼優化
使用逃逸分析,編譯器可以對代碼做如下優化:
① 棧上分配
? JIT編譯器在編譯期間根據逃逸分析的結果,發現如果一個對象并沒有逃逸出方法的話,就可能被優化成棧上分配。分配完成之后,繼續在調用棧內執行,最后線程結束,棧空間被回收,局部變量對象也被回收。這樣就無須進行垃圾回收了
? 常見的棧上分配場景:給成員變量賦值、方法返回值、實例引用傳遞
代碼分析:
以下代碼,關閉逃逸分析(-XX:-DoEscapeAnalysi),維護1000000個對象,如果開啟逃逸分析,只維護少量對象
啟動測試程序,這個時候程序中的線程處于睡眠狀態,進入cmd窗口中,通過jps和jmap來查看相應的內存分配情況。
關閉逃逸分析結果如下:
開啟逃逸分析結果如下:
② 同步省略
? 線程同步的代價是相當高的,同步的后果是降低并發性和性能
? 在動態編譯同步塊的時候,JIT編譯器可以借助逃逸分析來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問而沒有被發布到其他線程。如果沒有,那么JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提高并發性和性能。這個取消同步的過程就叫同步省略,也叫鎖消除
③ 分離對象或標量替換
? 標量Scalar是指一個無法再分解成更小的數據的數據。Java中的原始數據類型就是標量
? 相對的,那些還可以分解的數據叫做聚合量(Aggregate),Java中對象就是聚合量,因為它可以分解成其他聚合量和標量
? 在JIT階段,如果經過逃逸分析,發現一個對象不會被外界訪問的話,那么經過JIT優化,就會把這個對象拆解成若干個其中包含的若干個成員變量來替代。這個過程就是標量替換
以上代碼,經過標量替換后,就會變成:
public static void alloc(){int x = 1;int y = 2; }可以看到,Point這個聚合量經過逃逸分析后,發現他并沒有逃逸,就被替換成兩個標量了。那么標量替換有什么好處呢?就是可以大大減少堆內存的占用。因為一旦不需要創建對象了,那么就不再需要分配堆內存了。
???標量替換為棧上分配提供了很好的基礎。
測試代碼:
/*** 標量替換測試* -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations*/ public class ScalarReplace {public static class User {public int id;//標量(無法再分解成更小的數據)public String name;//聚合量(String還可以分解為char數組)}public static void alloc() {User u = new User();//未發生逃逸u.id = 5;u.name = "www.atguigu.com";}public static void main(String[] args) {long start = System.currentTimeMillis();for (int i = 0; i < 10000000; i++) {alloc();}long end = System.currentTimeMillis();System.out.println("花費的時間為: " + (end - start) + " ms");} }10.5 逃逸分析小結
? 關于逃逸分析的論文在1999年就已經發表了,但直到JDK1.6才有實現,而且這項技術到如今也并不是十分成熟的。
? 其根本原因就是無法保證逃逸分析的性能消耗一定能高于其他的消耗。雖然經過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列復雜的分析的,這其實也是一個相對耗時的過程。
? 一個極端的例子,就是經過逃逸分析之后,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。
? 雖然這項技術并不十分成熟,但是它也是即時編譯器優化技術中一個十分重要的手段。
? 注意到有一些觀點,認為通過逃逸分析,JVM會在棧上分配那些不會逃逸的對象,這在理論上是可行的,但是取決于JVM設計者的選擇。Oracle HotspotJVM中并未這么做,這一點在逃逸分析相關的文檔里已經說明,所以可以明確所有的對象實例都是創建在堆上。
? 目前很多書籍還是基于JDK7以前的版本,JDK已經發生了很大變化,intern字符串的緩存和靜態變量曾經都被分配在永久代上,而永久代已經被元數據區取代。但是,intern字符串緩存和靜態變量并不是被轉移到元數據區,而是直接在堆上分配,所以這一點同樣符合前面一點的結論:對象實例都是分配在堆上。
? 年輕代是對象的誕生、生長、消亡的區域,一個對象在這里產生、應用、最后被垃圾回收器收集、結束生命
? 老年代防止長生命周期對象,通常都是從Survivor區域篩選拷貝過來的Java對象。當然,也有特殊情況,我們知道普通的對象會被分配在TLAB上,如果對象較大,JVM會試圖直接分配在Eden其他位置上;如果對象再大,完全無法在新生代找到足夠長的連續空閑空間,JVM就會直接分配到老年代
? 當GC只發生在年輕代中,回收年輕對象的行為被稱為MinorGC。當GC發生在老年代時則被稱為MajorGC或者FullGC。一般的,MinorGC的發生頻率要比MajorGC高很多,即老年代中垃圾回收發生的頻率大大低于年輕代。
總結
以上是生活随笔為你收集整理的运行数据区②---堆的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LPM Sprint 4-13 开发 工
- 下一篇: 执行引擎