08-堆
堆
堆的核心概念
堆針對一個JVM進程來說是唯一的,也就是一個進程只有一個JVM,但是進程包含多個線程,他們是共享同一堆空間的。
一個JVM實例只存在一個堆內存,堆也是Java內存管理的核心區域。
Java堆區在JVM啟動的時候即被創建,其空間大小也就確定了。是JVM管理的最大一塊內存空間。
- 堆內存的大小是可以調節的。
《Java虛擬機規范》規定,堆可以處于物理上不連續的內存空間中,但在邏輯上它應該被視為連續的。
所有的線程共享Java堆,在這里還可以劃分線程私有的緩沖區(Thread Local Allocation Buffer,TLAB)。
-Xms10m:初始堆內存
-Xmx10m:最大堆內存
下圖就是使用:Java VisualVM查看堆空間的內容,通過 jdk bin提供的插件
《Java虛擬機規范》中對Java堆的描述是:所有的對象實例以及數組都應當在運行時分配在堆上。(The heap is the run-time data area from which memory for all class instances and arrays is allocated)
我要說的是:“幾乎”所有的對象實例都在這里分配內存。—從實際使用角度看的。
- 因為還有一些對象是在棧上分配的
數組和對象可能永遠不會存儲在棧上,因為棧幀中保存引用,這個引用指向對象或者數組在堆中的位置。
在方法結束后,堆中的對象不會馬上被移除,僅僅在垃圾收集的時候才會被移除。
- 也就是觸發了GC的時候,才會進行回收
- 如果堆中對象馬上被回收,那么用戶線程就會收到影響,因為有stop the word,頻繁的GC會影響用戶線程的執行
堆,是GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點區域。
堆的核心概述:堆內存細分
現代垃圾收集器大部分都基于分代收集理論設計,堆空間細分為:
Java 7及之前堆內存邏輯上分為三部分:新生區+養老區+永久區
- Young Generation Space 新生區 Young/New 又被劃分為Eden區和Survivor區
- Tenure generation space 養老區 Old/Tenure
- Permanent Space永久區 Perm
Java 8及之后堆內存邏輯上分為三部分:新生區+養老區+元空間
- Young Generation Space新生區 Young/New 又被劃分為Eden區和Survivor區
- Tenure generation space 養老區 Old/Tenure
- Meta Space 元空間 Meta
約定:新生區 -> 新生代 -> 年輕代 、 養老區 -> 老年區 -> 老年代、 永久區 -> 永久代
堆空間內部結構,JDK1.8之前從永久代 替換成 元空間
以上是邏輯上的劃分,實際上我們通過"-Xmx"和"-Xms"來進行設置的堆空間只是新生區+養老區的大小,暫時沒管永久代/元空間。
設置堆內存大小與OOM
Java堆區用于存儲Java對象實例,那么堆的大小在JVM啟動時就已經設定好了,大家可以通過選項"-Xmx"和"-Xms"來進行設置。
- “-Xms"用于表示堆區的起始內存,等價于-XX:InitialHeapSize
- “-Xmx"則用于表示堆區的最大內存,等價于-XX:MaxHeapSize
一旦堆區中的內存大小超過“-xmx"所指定的最大內存時,將會拋出outofMemoryError異常。
通常會將-Xms和-Xmx兩個參數配置相同的值,其目的是為了能夠在java垃圾回收機制清理完堆區后不需要重新分隔計算堆區的大小,即避免頻繁GC帶來的系統壓力,從而提高性能。
默認情況下
-
初始內存大小:物理電腦內存大小1/64
-
最大內存大小:物理電腦內存大小1/4
輸出結果
-Xms:245M -Xmx:3614M 系統內存大小為:15.3125G 系統內存大小為:14.152345375G可見,滿足:堆空間初始內存大小占物理電腦內存大小1/64,最大內存大小占物理電腦內存大小1/4
如何查看堆內存的內存分配情況
查看設置的參數:方式一:命令行:jps -> 查看當前正在運行的進程,得到相應進程idjstat -gc 進程id -> 查看該進程內存分配情況方式二:-XX:+PrintGCDetails /*** 設置-Xms600m -Xmx600m*/ 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;try{Thread.sleep(1000000);}catch(InterruptedException e){e.printStackTrace();}} }輸出結果
-Xms:575M -Xmx:575M可見,結果顯示的堆內存大小不等于我們設置的600M,原因:堆空間由伊甸園區和survivor 0區、survivor 1區組成,但存儲對象時,除伊甸園區外,我們只能在survivor 0區和survivor 1區中二選一,這是由于垃圾回收的復制算法。因此,顯示的堆空間是由伊甸園區加一個survivor區的空間所得。當然,以上三部分相加即為我們設置的總體600M。
堆空間即為新生代+老年代,上圖中OC、OU即為老年代,前者為老年代總共的空間,后者為使用的空間(used);新生代分為伊甸園區(Eden)、幸存者(Survivor)零區、幸存者(Survivor)一區,同樣“C”表示總體空間,“U”表示使用了的空間。
-XX:+PrintGCDetails
OutOfMemory舉例
我們簡單的寫一個OOM例子(不斷向堆中存數據)
/*** OOM測試*/ public class OOMTest {public static void main(String[] args) {List<Integer> list = new ArrayList<>();while(true) {list.add(999999999);}} }然后設置啟動參數
-Xms10m -Xmx:10m運行后,就出現OOM了,那么我們可以通過 VisualVM這個工具查看具體是什么參數造成的OOM
年輕代與老年代
存儲在JVM中的Java對象可以被劃分為兩類:
- 一類是生命周期較短的瞬時對象,這類對象的創建和消亡都非常迅速
- 生命周期短的,及時回收即可
- 另外一類對象的生命周期卻非常長,在某些極端的情況下還能夠與JVM的生命周期保持一致
Java堆區進一步細分的話,可以劃分為年輕代(YoungGen)和老年代(oldGen)
其中年輕代又可以劃分為Eden空間、Survivor0空間和Survivor1空間(有時也叫做from區、to區)
查看新生代與老年代在在堆結構的占比方式:
-
方式一:命令行
jps-->查看進程idjinfo -flag SurvivorRatio 進程id-->查看Eden空間和另外兩個survivor空間缺省所占的比例(默認8:1:1)jinfo -flag NewRatio 進程id-->新生代與老年代在在堆結構的占比(默認1 : 2)
-
方式二:可視化工具
-
方式三:-XX:+PrintGCDetails
運行程序,打印日志信息:
下面這參數開發中一般不會調:
默認情況下
- Eden:From:to -> 8:1:1
- 新生代:老年代 - > 1 : 2
配置新生代與老年代在堆結構的占比。
-
默認-XX:NewRatio=2,表示新生代占1,老年代占2,新生代占整個堆的1/3
-
可以修改-XX:NewRatio=4,表示新生代占1,老年代占4,新生代占整個堆的1/5
當發現在整個項目中,生命周期長的對象偏多,那么就可以通過調整 老年代的大小,來進行調優
在HotSpot中,Eden空間和另外兩個survivor空間缺省所占的比例是8:1:1,當然開發人員可以通過選項“-xx:SurvivorRatio”調整這個空間比例。比如-XX:SurvivorRatio=8。
這里需要注意,雖然默認Eden空間和另外兩個survivor空間缺省所占的比例是8:1:1,通過上面的命令行顯示也是8:1:1。但是,事實上,存在自適應機制,導致這個值事實上不是8:1:1,可通過命令行以及Visual GC工具手動計算出來,如下例,自適應的比例為6:1:1。
網上一些資料說,如果想恢復默認的8:1:1比例,我們應當顯式地關閉自適應內存分配策略,即:-XX:-UseAdaptiveSizePolicy(第二個減號即意味著關閉,若為加號則意味使用),但實際使用會發現沒用,還是之前6:1:1。因而,我們如果想恢復默認的8:1:1比例,必須顯式地指定比例為8:1:1,即通過“-XX:SurvivorRatio”調整這個空間比例,比如-XX:SurvivorRatio=8。
小結:
- -XX:NewRatio : 設置新生代與老年代的比例,默認值為2
- -XX:SurvivorRatio : 設置新生代中Eden區與Survivor區的比例,默認值為8
- 由于自適應內存分配策略,事實上需顯式指定才能為默認的8:1:1
- -XX:-UseAdaptiveSizePolicy : 關閉自適應內存分配策略(暫時用不到)
- 幾乎所有的Java對象都是在Eden區被new出來的。(有些大的對象在Eden區無法存儲時候,將直接進入老年代)
- 絕大部分的Java對象的銷毀都在新生代進行了。
IBM公司的專門研究表明,新生代中80%的對象都是“朝生夕死”的。
可以使用選項"-Xmn"設置新生代最大內存大小
當同時設置-XX:NewRatio和-Xmn時,以-Xmn顯式指定的內存大小為準。
不過這個參數一般使用默認值就可以了,一般不設置。
圖解對象分配過程
概念
為新對象分配內存是一件非常嚴謹和復雜的任務,JM的設計者們不僅需要考慮內存如何分配、在哪里分配等問題,并且由于內存分配算法與內存回收算法密切相關,所以還需要考慮GC執行完內存回收后是否會在內存空間中產生內存碎片。
- new的對象先放伊甸園區。此區有大小限制。
- 當伊甸園的空間填滿時,程序又需要創建對象,JVM的垃圾回收器將對伊甸園區進行垃圾回收(MinorGC),將伊甸園區中的不再被其他對象所引用的對象進行銷毀。再加載新的對象放到伊甸園區
- 然后將伊甸園中的剩余對象移動到幸存者0區。
- 如果再次觸發垃圾回收,此時上次幸存下來的放到幸存者0區的,如果沒有回收,就會放到幸存者1區。
- 如果再次經歷垃圾回收,此時會重新放回幸存者0區,接著再去幸存者1區。
- 啥時候能去養老區呢?可以設置次數。默認是15次。
- 可以設置參數:-XX:MaxTenuringThreshold= N進行設置
- 在養老區,相對悠閑。當養老區內存不足時,再次觸發GC:Major GC,進行養老區的內存清理
- 若養老區執行了Major GC之后,發現依然無法進行對象的保存,就會產生OOM異常。
圖解過程
我們創建的對象,一般都是先存放在Eden區的,當我們Eden區滿了后,就會觸發GC操作,一般被稱為 YGC / Minor GC操作(Young GC)
當我們進行一次垃圾收集后,紅色的將會被回收,而綠色的還會被占用著,存放在幸存者S0(Survivor From)區。同時我們給每個對象設置了一個年齡計數器,一次回收后就是1。(此時,Eden區清空。此時,s1空,所以此時s1為to區,s0為from區。)
同時Eden區繼續存放對象,當Eden區再次存滿的時候,又會觸發一個MinorGC操作,此時GC將會把 Eden和Survivor From中的對象 進行一次收集,把存活的對象放到 Survivor To區,同時讓年齡 + 1。(此時,s0清空,所以此時s0變為to區,s1為from區。每一次執行GC后,誰空誰就是to區,to區即代表下一次GC時,Eden區的對象要往哪放)
我們繼續不斷的進行對象生成 和 垃圾回收,當Survivor中的對象的年齡達到15的時候,將會觸發一次 Promotion晉升的操作,也就是將年輕代中的對象 晉升到 老年代中
總結:
- 針對幸存者s0,s1區的總結:復制之后有交換,誰空誰是to。
- 關于垃圾回收:頻繁在新生區回收,很少在老年區收集,幾乎不在永久區/元空間收集。
- 新生代采用復制算法的目的:是為了減少內存碎片
思考:幸存區區滿了后?
特別注意,在Eden區滿了的時候,才會觸發MinorGC,而幸存者區滿了后,不會觸發MinorGC操作
如果Survivor區滿了后,將會觸發一些特殊的規則,也就是可能直接晉升老年代
舉例:以當兵為例,正常人的晉升可能是 : 新兵 -> 班長 -> 排長 -> 連長
但是也有可能有些人因為做了非常大的貢獻,直接從 新兵 -> 排長
對象分配的特殊情況
代碼演示對象分配過程
我們不斷的創建大對象
/*** 代碼演示對象創建過程*/ public class HeapInstanceTest {byte [] buffer = new byte[new Random().nextInt(1024 * 200)];public static void main(String[] args) throws InterruptedException {ArrayList<HeapInstanceTest> list = new ArrayList<>();while (true) {list.add(new HeapInstanceTest());Thread.sleep(10);}} }然后設置JVM參數
-Xms600m -Xmx600m然后cmd輸入下面命令,打開VisualVM圖形化界面
jvisualvm然后通過執行上面代碼,通過VisualGC進行動態化查看
最終,在老年代和新生代都滿了,就出現OOM,這里主要是因為老年代滿了
Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat com.atguigu.java.chapter08.HeapInstanceTest.<init>(HeapInstanceTest.java:13)at com.atguigu.java.chapter08.HeapInstanceTest.main(HeapInstanceTest.java:17)常用的調優工具
- JDK命令行
- Eclipse:Memory Analyzer Tool
- Jconsole
- Visual VM(實時監控 推薦~)
- Jprofiler(推薦~)
- Java Flight Recorder(實時監控)
- GCViewer
- GCEasy
Minor GC,MajorGC、Full GC
- Minor GC(YGC):新生代的GC
- Major GC:老年代的GC
- Full GC:整堆收集,收集整個Java堆和方法區的垃圾收集
我們都知道,JVM的調優的一個環節,也就是垃圾收集,我們需要盡量的避免垃圾回收,因為在垃圾回收的過程中,容易出現STW(stop the world,中止用戶線程)的問題
而 Major GC 和 Full GC出現STW的時間,是Minor GC的10倍以上,因此,主要關注 Major GC 和 Full GC
Stop the World機制,簡稱STW,即在執行垃圾收集算法時,Java應用程序的其他所有除了垃圾收集收集器線程之外的線程都被掛起
JVM在進行GC時,并非每次都對上面三個內存(新生代、老年代;方法區)區域一起回收的,大部分時候回收的都是指新生代。針對Hotspot VM的實現,它里面的GC按照回收區域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)
部分收集:不是完整收集整個Java堆的垃圾收集。其中又分為:
- 新生代收集(Minor GC/Young GC):只是新生代(Eden\S0,S1)的垃圾收集
- 老年代收集(Major GC/old GC):只是老年代的圾收集。
- 目前,只有CMS GC會有單獨收集老年代的行為。
- 注意,很多時候Major GC會和Full GC混淆使用,需要具體分辨是老年代回收還是整堆回收。
- 混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集。
- 目前,只有G1 GC會有這種行為
整堆收集(FullGC):收集整個java堆和方法區的垃圾收集。
Minor GC
新生代GC(Minor GC/YGC)觸發機制:
當年輕代空間不足時,就會觸發Minor GC,這里的年輕代滿指的是Eden代滿,Survivor滿不會引發GC。(每次Minor GC會清理年輕代的內存。)
因為Java對象大多都具備 朝生夕滅 的特性,所以Minor GC非常頻繁,一般回收速度也比較快。這一定義既清晰又易于理解。
Minor GC會引發STW,暫停其它用戶的線程,等垃圾回收結束,用戶線程才恢復運行
STW:stop the word
Major GC
老年代GC(Major GC/old GC)觸發機制:
指發生在老年代的GC,對象從老年代消失時,我們說 “Major GC” 或 “Full GC” 發生了
出現了MajorGC,經常會伴隨至少一次的Minor GC(但非絕對的,在Paralle1 Scavenge收集器的收集策略里就有直接進行MajorGC的策略選擇過程)
- 也就是在老年代空間不足時,會先嘗試觸發MinorGC。如果之后空間還不足,則觸發Major GC
Major GC的速度一般會比MinorGC慢10倍以上,STW的時間更長,如果Major GC后,內存還不足,就報OOM了
Full GC
觸發Full GC執行的情況有如下五種:
- 調用System.gc()時,系統建議執行Full GC,但是不必然執行
- 老年代空間不足
- 方法區空間不足
- 通過Minor GC后進入老年代的平均大小大于老年代的可用內存
- 由Eden區、survivor space0(From Space)區向survivor space1(To Space)區復制時,對象大小大于To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小于該對象大小
說明:Full GC 是開發或調優中盡量要避免的。這樣暫時時間會短一些
GC 舉例
我們編寫一個OOM的異常,因為我們在不斷的創建字符串,是存放在元空間的
/*** GC測試*/ public class GCTest {public static void main(String[] args) {int i = 0;try {List<String> list = new ArrayList<>();String a = "mogu blog";while(true) {list.add(a);a = a + a;i++;}}catch (Exception e) {e.getStackTrace();}} }設置JVM啟動參數
-Xms10m -Xmx10m -XX:+PrintGCDetails打印出的日志
[GC (Allocation Failure) [PSYoungGen: 2038K->500K(2560K)] 2038K->797K(9728K), 0.3532002 secs] [Times: user=0.01 sys=0.00, real=0.36 secs] [GC (Allocation Failure) [PSYoungGen: 2108K->480K(2560K)] 2405K->1565K(9728K), 0.0014069 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Ergonomics) [PSYoungGen: 2288K->0K(2560K)] [ParOldGen: 6845K->5281K(7168K)] 9133K->5281K(9728K), [Metaspace: 3482K->3482K(1056768K)], 0.0058675 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] [GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 5281K->5281K(9728K), 0.0002857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 5281K->5263K(7168K)] 5281K->5263K(9728K), [Metaspace: 3482K->3482K(1056768K)], 0.0058564 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] HeapPSYoungGen total 2560K, used 60K [0x00000000ffd00000, 0x0000000100000000, 0x0000000100000000)eden space 2048K, 2% used [0x00000000ffd00000,0x00000000ffd0f138,0x00000000fff00000)from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)ParOldGen total 7168K, used 5263K [0x00000000ff600000, 0x00000000ffd00000, 0x00000000ffd00000)object space 7168K, 73% used [0x00000000ff600000,0x00000000ffb23cf0,0x00000000ffd00000)Metaspace used 3514K, capacity 4498K, committed 4864K, reserved 1056768Kclass space used 388K, capacity 390K, committed 512K, reserved 1048576KException in thread "main" java.lang.OutOfMemoryError: Java heap spaceat java.util.Arrays.copyOfRange(Arrays.java:3664)at java.lang.String.<init>(String.java:207)at java.lang.StringBuilder.toString(StringBuilder.java:407)at com.atguigu.java.chapter08.GCTest.main(GCTest.java:20)觸發OOM的時候,一定是進行了一次Full GC,因為只有在老年代空間不足時候,才會爆出OOM異常
堆空間分代思想
為什么要把Java堆分代?不分代就不能正常工作了嗎?經研究,不同對象的生命周期不同。70%-99%的對象是臨時對象。
新生代:有Eden、兩塊大小相同的survivor(又稱為from/to,s0/s1)構成,to總為空。
老年代:存放新生代中經歷多次GC仍然存活的對象。
其實不分代完全可以,分代的唯一理由就是優化GC性能。如果沒有分代,那所有的對象都在一塊,就如同把一個學校的人都關在一個教室。GC的時候要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描。而很多對象都是朝生夕死的,如果分代的話,把新創建的對象放到某一地方,當GC的時候先把這塊存儲“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。
內存分配策略(或對象提升(Promotion)規則)
如果對象在Eden出生并經過第一次Minor GC后仍然存活,并且能被Survivor容納的話,將被移動到survivor空間中,并將對象年齡設為1。對象在survivor區中每熬過一次MinorGC,年齡就增加1歲,當它的年齡增加到一定程度(默認為15歲,其實每個JVM、每個GC都有所不同)時,就會被晉升到老年代
對象晉升老年代的年齡閥值,可以通過選項**-XX:MaxTenuringThreshold**來設置
針對不同年齡段的對象分配原則如下所示:
- 優先分配到Eden
- 開發中比較長的字符串或者數組,會直接存在老年代,但是因為新創建的對象 都是 朝生夕死的,所以這個大對象可能也很快被回收,如此為了存儲該大對象承受的STW就顯得很可惜。但是因為老年代觸發Major GC的次數比 Minor GC要更少,因此可能回收起來就會比較慢
- 大對象直接分配到老年代
- 盡量避免程序中出現過多的大對象
- 長期存活的對象分配到老年代
- 動態對象年齡判斷
- 如果survivor區中相同年齡的所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象可以直接進入老年代,無須等到MaxTenuringThreshold 中要求的年齡。
空間分配擔保: -XX:HandlePromotionFailure
- 也就是經過Minor GC后,所有的對象都存活,因為Survivor比較小,所以就需要將Survivor無法容納的對象,存放到老年代中。
為對象分配內存:TLAB
問題:堆空間都是共享的么?
不一定,因為還有TLAB這個概念,在堆中劃分出一塊區域,為每個線程所獨占
為什么有TLAB?
TLAB:Thread Local Allocation Buffer,也就是為每個線程單獨分配了一個緩沖區
堆區是線程共享區域,任何線程都可以訪問到堆區中的共享數據
由于對象實例的創建在JVM中非常頻繁,因此在并發環境下從堆區中劃分內存空間是線程不安全的
為避免多個線程操作同一地址,需要使用加鎖等機制,進而影響分配速度。
什么是TLAB
從內存模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM為每個線程分配了一個私有緩存區域,它包含在Eden空間內。
多線程同時分配內存時,使用TLAB可以避免一系列的非線程安全問題,同時還能夠提升內存分配的吞吐量,因此我們可以將這種內存分配方式稱之為快速分配策略。
據我所知所有OpenJDK衍生出來的JVM都提供了TLAB的設計。
盡管不是所有的對象實例都能夠在TLAB中成功分配內存,但JVM確實是將TLAB作為內存分配的首選。
在程序中,開發人員可以通過選項“-XX:UseTLAB”設置是否開啟TLAB空間,默認開啟。
默認情況下,TLAB空間的內存非常小,僅占有整個Eden空間的1%,當然我們可以通過選項“-Xx:TLABWasteTargetPercent”設置TLAB空間所占用Eden空間的百分比大小。
一旦對象在TLAB空間分配內存失敗時,JVM就會嘗試著通過使用加鎖機制確保數據操作的原子性,從而直接在Eden空間中分配內存。
TLAB分配過程
對象首先是通過TLAB開辟空間,如果不能放入,那么需要通過Eden來進行分配。當然,對于一些大對象,我們直接放在老年區。
小結:堆空間的參數設置
-
-XX:+PrintFlagsInitial:查看所有的參數的默認初始值
-
-XX:+PrintFlagsFinal:查看所有的參數的最終值(可能會存在修改,不再是初始值,前面有“:"即說明存在重新賦值)
相當于具體查看某個參數的指令:
jps:查看當前 運行中的進程
jinfo -flag 要查看的參數(如:SurvivorRatio) 進程id -
-Xms:初始堆空間內存(默認為物理內存的1/64)
-
-Xmx:最大堆空間內存(默認為物理內存的1/4)
-
-Xmn:設置新生代的大小。(初始值及最大值)
-
-XX:NewRatio:配置新生代與老年代在堆結構的占比
-
-XX:SurvivorRatio:設置新生代中Eden和S0/S1空間的比例
-
-XX:MaxTenuringThreshold:設置新生代垃圾的最大年齡
-
-XX:+PrintGCDetails:輸出詳細的GC處理日志
- 打印gc簡要信息:①-XX:+PrintGC ② - verbose:gc
-
-XX:HandlePromotionFalilure:是否設置空間分配擔保
在發生Minor GC之前,虛擬機會檢查老年代最大可用的連續空間是否大于新生代所有對象的總空間。
- 如果大于,則此次Minor GC是安全的
- 如果小于,則虛擬機會查看-xx:HandlePromotionFailure設置值是否允擔保失敗。
- 如果HandlePromotionFailure=true,那么會繼續檢查老年代最大可用連續空間是否大于歷次晉升到老年代的對象的平均大小。
- 如果大于,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的;
- 如果小于,則改為進行一次FullGC。
- 如果HandlePromotionFailure=false,則改為進行一次Full Gc。
- 如果HandlePromotionFailure=true,那么會繼續檢查老年代最大可用連續空間是否大于歷次晉升到老年代的對象的平均大小。
在JDK6 Update24(JDK7)之后,HandlePromotionFailure參數不會再影響到虛擬機的空間分配擔保策略,觀察openJDK中的源碼變化,雖然源碼中還定義了HandlePromotionFailure參數,但是在代碼中已經不會再使用它。JDK6 Update 24之后的規則變為只要老年代的連續空間大于新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行FullGC,即理解恒為ture即可。
堆是分配對象的唯一選擇么?
逃逸分析
在《深入理解Java虛擬機》中關于Java堆內存有這樣一段描述:
隨著JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那么“絕對”了。
在Java虛擬機中,對象是在Java堆中分配內存的,這是一個普遍的常識。但是,有一種特殊情況,那就是**如果經過逃逸分析(Escape Analysis)后發現,一個對象并沒有逃逸出方法的話,那么就可能被優化成棧上分配。**這樣就無需在堆上分配內存,也無須進行垃圾回收了。這也是最常見的堆外存儲技術。
此外,前面提到的基于openJDk深度定制的TaoBaovm,其中創新的GCIH(GC invisible heap)技術實現off-heap,將生命周期較長的Java對象從heap中移至heap外,并且GC不能管理GCIH內部的Java對象,以此達到降低GC的回收頻率和提升GC的回收效率的目的。
如何將堆上的對象分配到棧,需要使用逃逸分析手段。
這是一種可以有效減少Java程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。
通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用的使用范圍從而決定是否要將這個對象分配到堆上。
逃逸分析的基本行為就是分析對象動態作用域:
- 當一個對象在方法中被定義后,對象只在方法內部使用,則認為沒有發生逃逸。
- 當一個對象在方法中被定義后,它被外部方法所引用,則認為發生逃逸。例如作為調用參數傳遞到其他地方中。
逃逸分析舉例
沒有發生逃逸的對象,則可以分配到棧上,隨著方法執行的結束,棧空間就被移除,每個棧里面包含了很多棧幀,也就是發生逃逸分析
public void my_method() {V v = new V();// use v// ....v = null; }針對下面的代碼
public static StringBuffer createStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb; }如果想要StringBuffer sb不發生逃逸,可以這樣寫
public static String createStringBuffer(String s1, String s2) {StringBuffer sb = new StringBuffer();sb.append(s1);sb.append(s2);return sb.toString(); }完整的逃逸分析代碼舉例
/*** 逃逸分析* 如何快速的判斷是否發生了逃逸分析,大家就看new的對象實體是否在方法外被調用。*/ public class EscapeAnalysis {public EscapeAnalysis obj;/*** 方法返回EscapeAnalysis對象,發生逃逸* @return*/public EscapeAnalysis getInstance() {return obj == null ? new EscapeAnalysis():obj;}/*** 為成員屬性賦值,發生逃逸*/public void setObj() {this.obj = new EscapeAnalysis();}/*** 對象的作用于僅在當前方法中有效,沒有發生逃逸*/public void useEscapeAnalysis() {EscapeAnalysis e = new EscapeAnalysis();}/*** 引用成員變量的值,發生逃逸*/public void useEscapeAnalysis2() {EscapeAnalysis e = getInstance();// getInstance().XXX 發生逃逸} }參數設置
在JDK 1.7 版本之后,HotSpot中默認就已經開啟了逃逸分析
如果使用的是較早的版本,開發人員則可以通過:
- 選項“-XX:+DoEscapeAnalysis"顯式開啟逃逸分析
- 通過選項“-XX:+PrintEscapeAnalysis"查看逃逸分析的篩選結果
結論
開發中能使用局部變量的,就不要使用在方法外定義。
使用逃逸分析,編譯器可以對代碼做如下優化:
- 棧上分配:將堆分配轉化為棧分配。如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會發生逃逸,對象可能是棧上分配的候選,而不是堆上分配
- 同步省略:如果一個對象被發現只有一個線程被訪問到,那么對于這個對象的操作可以不考慮同步。
- 分離對象或標量替換:有的對象可能不需要作為一個連續的內存結構存在也可以被訪問到,那么對象的部分(或全部)可以不存儲在內存(java:堆),而是存儲在CPU寄存器(Java:棧)中。
棧上分配
JIT編譯器在編譯期間根據逃逸分析的結果,發現如果一個對象并沒有逃逸出方法的話,就可能被優化成棧上分配。分配完成后,繼續在調用棧內執行,最后線程結束,棧空間被回收,局部變量對象也被回收。這樣就無須進行垃圾回收了(棧上不需要GC)。
常見的發生逃逸,不能棧上分配的場景
在逃逸分析中,已經說明了。分別是給成員變量賦值、方法返回值、實例引用傳遞。
舉例
我們通過舉例來說明 開啟逃逸分析 和 未開啟逃逸分析時候的情況
/*** 棧上分配* -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails*/ class User {private String name;private String age;private String gender;private String phone; } public class StackAllocation {public static void main(String[] args) throws InterruptedException {long start = System.currentTimeMillis();for (int i = 0; i < 100000000; i++) {alloc();}//查看執行時間long end = System.currentTimeMillis();System.out.println("花費的時間為:" + (end - start) + " ms");// 為了方便查看堆內存中對象個數,線程sleepThread.sleep(10000000);}private static void alloc() {User user = new User();//未發生逃逸} }設置JVM參數,表示未開啟逃逸分析
-Xmx256m -Xms256m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails運行結果,同時還觸發了GC操作
花費的時間為:664 ms然后查看內存的情況,發現有大量的User存儲在堆中
我們在開啟逃逸分析
-Xmx256m -Xms256m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails然后查看運行時間,我們能夠發現花費的時間快速減少,同時不會發生GC操作
花費的時間為:5 ms然后在看內存情況,我們發現只有很少的User對象,說明User發生了逃逸,因為他們存儲在棧中,隨著棧的銷毀而消失
同步省略
線程同步的代價是相當高的,同步的后果是降低并發性和性能。
在動態編譯同步塊的時候,JIT編譯器可以借助逃逸分析來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問而沒有被發布到其他線程。如果沒有,那么JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提高并發性和性能。這個取消同步的過程就叫同步省略,也叫鎖消除。
例如下面的代碼
public void f() {Object hellis = new Object();synchronized(hellis) {System.out.println(hellis);} }代碼中對hellis這個對象加鎖,但是hellis對象的生命周期只在f()方法中,并不會被其他線程所訪問到,所以在JIT編譯階段就會被優化掉,優化成:
public void f() {Object hellis = new Object();System.out.println(hellis); }我們將其轉換成字節碼
分離對象和標量替換
**標量(scalar)**是指一個無法再分解成更小的數據的數據。Java中的原始數據類型就是標量。
相對的,那些還可以分解的數據叫做聚合量(Aggregate),Java中的對象就是聚合量,因為他可以分解成其他聚合量和標量。
在JIT階段,如果經過逃逸分析,發現一個對象不會被外界訪問的話,那么經過J工T優化,就會把這個對象拆解成若干個其中包含的若干個成員變量來代替。這個過程就是標量替換。
public static void main(String args[]) {alloc(); } class Point {private int x;private int y; } private static void alloc() {Point point = new Point(1,2);System.out.println("point.x" + point.x + ";point.y" + point.y); }以上代碼,經過標量替換后,就會變成
private static void alloc() {int x = 1;int y = 2;System.out.println("point.x = " + x + "; point.y=" + y); }可以看到,Point這個聚合量經過逃逸分析后,發現他并沒有逃逸,就被替換成兩個聚合量了。那么標量替換有什么好處呢?就是可以大大減少堆內存的占用。因為一旦不需要創建對象了,那么就不再需要分配堆內存了,同時由于直接在棧上分配,也避免了GC。
標量替換為棧上分配提供了很好的基礎。
標量替換參數設置
參數-XX:+EliminateAllocations:開啟了標量替換(默認打開),允許將對象打散分配在棧上,相當于變形的棧上分配。
可見,使用標量替換,大大節省運行時間,且無需GC(棧不需要GC)。
代碼優化之標量替換
上述代碼在主函數中進行了1億次alloc。調用進行對象創建,由于User對象實例需要占據約16字節的空間,因此累計分配空間達到將近1.5GB。如果堆空間小于這個值,就必然會發生GC。使用如下參數運行上述代碼:
-server -Xmx100m -Xms100m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations這里設置參數如下:
- 參數-server:啟動Server模式,因為在server模式下,才可以啟用逃逸分析。(64位默認即為Server模式)
- 參數-XX:+DoEscapeAnalysis:啟用逃逸分析
- 參數-Xmx10m:指定了堆空間最大為10MB
- 參數-XX:+PrintGC:將打印GC日志。
- 參數一xx:+EliminateAllocations:開啟了標量替換(默認打開),允許將對象打散分配在棧上,比如對象擁有id和name兩個字段,那么這兩個字段將會被視為兩個獨立的局部變量進行分配
逃逸分析小結:逃逸分析并不成熟
關于逃逸分析的論文在1999年就已經發表了,但直到JDK1.6才有實現,而且這項技術到如今也并不是十分成熟的。
其根本原因就是無法保證逃逸分析的性能消耗一定能高于他的消耗。雖然經過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列復雜的分析的,這其實也是一個相對耗時的過程。
一個極端的例子,就是經過逃逸分析之后,發現沒有一個對象是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。
雖然這項技術并不十分成熟,但是它也是即時編譯器優化技術中一個十分重要的手段。注意到有一些觀點,認為通過逃逸分析,JVM會在棧上分配那些不會逃逸的對象,這在理論上是可行的,但是取決于JvM設計者的選擇。據我所知,oracle Hotspot JVM中并未這么做,這一點在逃逸分析相關的文檔里已經說明,所以可以明確所有的對象實例都是創建在堆上,上述代碼(棧上分配等)驗證的效果實際上都是由于標量替換所致。
目前很多書籍還是基于JDK7以前的版本,JDK已經發生了很大變化,intern字符串的緩存和靜態變量曾經都被分配在永久代上,而永久代已經被元數據區取代。但是,intern字符串緩存和靜態變量并不是被轉移到元數據區,而是直接在堆上分配,所以這一點同樣符合前面一點的結論:對象實例都是分配在堆上。
小結
年輕代是對象的誕生、成長、消亡的區域,一個對象在這里產生、應用,最后被垃圾回收器收集、結束生命。
老年代放置長生命周期的對象,通常都是從survivor區域篩選拷貝過來的Java對象。當然,也有特殊情況,我們知道普通的對象會被分配在TLAB上;如果對象較大,JVM會試圖直接分配在Eden其他位置上;如果對象太大,完全無法在新生代找到足夠長的連續空閑空間,JVM就會直接分配到老年代。
當GC只發生在年輕代中,回收年輕代對象的行為被稱為MinorGC。當GC發生在老年代時則被稱為MajorGC或者FullGC。一般的,MinorGC的發生頻率要比MajorGC高很多,即老年代中垃圾回收發生的頻率將大大低于年輕代。
總結
- 上一篇: 543. 二叉树的直径
- 下一篇: 09-方法区