在《Java對象在Java虛擬機中的創建過程》了解到對象創建的內存分配,在《Java內存區域 JVM運行時數據區》中了解到各數據區有些什么特點、以及相關參數的調整,在《Java虛擬機垃圾回收(一) 基礎》中了解到如何判斷對象是存活還是已經死亡?在《Java虛擬機垃圾回收(二) 垃圾回收算法》了解到Java虛擬機垃圾回收的幾種常見算法,在《Java虛擬機垃圾回收(三) 7種垃圾收集器》了解到幾種收集器的特點和應用等。
下面來了解總結前面的一些內容:主要包括內存分配與回收策略、方法區垃圾回收、以及JVM垃圾回收的調優方法、垃圾收集器選擇。
1、內存分配與回收策略
通過在《Java虛擬機垃圾回收(二) 垃圾回收算法》"4、分代收集算法"中,我們知道目前幾乎所有商業虛擬機的垃圾收集器都采用分代收集算法,對於HotSpot一般的年代內存划分,如下圖:
對象的內存分配從大體上講:
在堆上分配(JIT編譯優化后可能在棧上分配),主要在新生代的Eden區中分配;
如果啟用了本地線程分配緩沖,將線程優先在TLAB上分配;
少數情況下,可能直接分配在老年代中。
分配的細節取決於當前使用哪種垃圾收集器組合,以及JVM中內存相關參數設置。
接下來將會講解幾條最普遍的內存分配規則。
1-1、對象優先在Eden分配
前面文章曾介紹HotSpot虛擬機新生代內存布局及算法
(1)、將新生代內存分為一塊較大的Eden空間和兩塊較小的Survivor空間;
(2)、每次使用Eden和其中一塊Survivor;
(3)、當回收時,將Eden和使用中的Survivor中還存活的對象一次性復制到另外一塊Survivor;
(4)、而后清理掉Eden和使用過的Survivor空間;
(5)、后面就使用Eden和復制到的那一塊Survivor空間,重復步驟3;
默認Eden:Survivor=8:1,即每次可以使用90%的空間,只有一塊Survivor的空間被浪費;
大多數情況下,對象在新生代Eden區中分配;
當Eden區沒有足夠空間進行分配時,JVM將發起一次Minor GC(新生代GC);
Minor GC時,如果發現存活的對象無法全部放入Survivor空間,只好通過分配擔保機制提前轉移到老年代。
1-2、大對象直接進入老年代
大對象指需要大量連續內存空間的Java對象,如,很長的字符串、數組;
經常出現大對象容易導致內存還有不少空間就提前觸發GC,以獲取足夠的連續空間來存放它們,所以應該盡量避免使用創建大對象;
"-XX:PretenureSizeThreshold":
可以設置這個閾值,大於這個參數值的對象直接在老年代分配;
默認為0(無效),且只對Serail和ParNew兩款收集器有效;
如果需要使用該參數,可考慮ParNew+CMS組合。
1-3、長期存活的對象將進入老年代
JVM給每個對象定義一個對象年齡計數器,其計算流程如下:
在Eden中分配的對象,經Minor GC后還存活,就復制移動到Survivor區,年齡為1;
而后每經一次Minor GC后還存活,在Survivor區復制移動一次,年齡就增加1歲;
如果年齡達到一定程度,就晉升到老年代中;
"-XX:MaxTenuringThreshold":
設置新生代對象晉升老年代的年齡閾值,默認為15;
1-4、動態對象年齡判定
JVM為更好適應不同程序,不是永遠要求等到MaxTenuringThreshold中設置的年齡;
如果在Survivor空間中相同年齡的所有對象大小總和大於Survivor空間的一半,大於或等於該年齡的對象就可以直接進入老年代;
1-5、空間分配擔保
在前面曾簡單介紹過分配擔保:
當Survivor空間不夠用時,需要依賴其他內存(老年代)進行分配擔保(Handle Promotion);
分配擔保的流程如下:
在發生Minor GC前,JVM先檢查老年代最大可用的連續空間是否大於新生所有對象空間;
如果大於,那可以確保Minor GC是安全的;
如果不大於,則JVM查看HandlePromotionFailure值是否允許擔保失敗;
如果允許,就繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代對象的平均大小;
如果大於,將嘗試進行一次Minor GC,但這是有風險的;
如果小於或HandlePromotionFailure值不允許冒險,那這些也要改為進行一次Full GC;
嘗試Minor GC的風險--擔保失敗:
因為嘗試Minor GC前面,無法知道存活的對象大小,所以使用歷次晉升到老年代對象的平均大小作為經驗值;
假如嘗試的Minor GC最終存活的對象遠遠高於經驗值的話,會導致擔保失敗(Handle Promotion Failure);
失敗后只有重新發起一次Full GC,這繞了一個大圈,代價較高;
但一般還是要開啟HandlePromotionFailure,避免Full GC過於頻繁,而且擔保失敗概率還是比較低的;
JDK6-u24后,JVM代碼中已經不再使用HandlePromotionFailure參數了;
規則變為:
只要老年代最大可用的連續空間大於新生所有對象空間或歷次晉升到老年代對象的平均大小,就會進行Minor GC;否則進行Full GC;
即老年代最大可用的連續空間小於新生所有對象空間時,不再檢查HandelPromotionFailure,而直接檢查歷次晉升到老年代對象的平均大小;
2、回收方法區
在《Java內存區域 JVM運行時數據區》曾介紹過方法區及相關的回收問題,雖然JVM規范規定這個區域可以不實現垃圾收集,且針對常量池和類型卸載的收回效果不佳,但方法區實現垃圾回收是必要的,下面再來詳細了解。
2-1、方法區(永久代)的主要回收對象
1、廢棄常量
與回收Java堆中對象非常類似;
2、無用的類
同時滿足下面3個條件才能算"無用的類":
(1)、該類所有實例都已經被回收(即Java椎中不存在該類的任何實例);
(2)、加載該類的ClassLoader已經被回收,也即通過引導程序加載器加載的類不能被回收;
(3)、該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法;
2-2、需要注意方法區回收的應用
在大量使用反射、動態代理、經常動態生成大量類的應用,要注意類的回收;
如運行時動態生成類的應用:
1、CGLib在Spring、Hibernate等框架中對類進行增強時會使用;
2、VM的動態語言也會動態創建類來實現語言的動態性;
3、另外,JSP(第一次使用編譯為Java類)、基於OSGi頻繁自定義ClassLoader的應用(同一個類文件,不同加載器加載視為不同類)等;
2-3、HotSpot虛擬機的相關調整
1、在JDK7中
使用永久代(Permanent Generation)實現方法區,這樣就可以不用專門實現方法區的內存管理,但這容易引起內存溢出問題;
有規划放棄永久代而改用Native Memory來實現方法區;
不再在Java堆的永久代中生成中分配字符串常量池,而是在Java堆其他的主要部分(年輕代和老年代)中分配;
更多請參考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html
2、在JDK8中
永久代已被刪除,類元數據(Class Metadata)存儲空間在本地內存中分配,並用顯式管理元數據的空間:
從OS請求空間,然后分成塊;
類加載器從它的塊中分配元數據的空間(一個塊被綁定到一個特定的類加載器);
當為類加載器卸載類時,它的塊被回收再使用或返回到操作系統;
元數據使用由mmap分配的空間,而不是由malloc分配的空間;
3、相關參數
"-XX:MaxMetaspaceSize" (JDK8):指定類元數據區的最大內存大小;
"-XX:MetaspaceSize" (JDK8):指定類元數據區的內存閾值--超過將觸發垃圾回收;
"-Xnolassgc":控制是否對類進行回收;
"-verbose:class"、"-XX:TraceClassLoading"、"-XX:TraceClassUnloading":查看類加載和卸載信息;
更多請參考:
《Java語言規范》12.7 卸載類和接口;
JDK8類元數據說明: http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/considerations.html#sthref62
3、JVM垃圾回收的調優方法
內存回收與垃圾收集器是影響系統性能、並發能力的主要因素之一,一般都需要進行一些手動的測試、調整優化;
下面介紹的是一些思路,並非是具體的參數設置。
3-1、明確期望的目標(關注點)
首先應該明確我們的應用程序調整垃圾回收期望的目標(關注點)是什么?
在前文曾介紹過通常有這些關注點:
(1)、停頓時間
GC停頓時間越短就適合需要與用戶交互的程序,良好的響應速度能提升用戶體驗;
與用戶交互較多的場景,以給用戶帶來較好的體驗;
如常見WEB、B/S系統的服務器上的應用;
(2)、吞吐量
吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間);
高吞吐量可以高效率地利用CPU時間,盡快完成運算的任務,主要適合在后台計算而不需要太多交互的任務;
應用程序運行在具有多個CPU上,對暫停時間沒有特別高的要求;
程序主要在后台進行計算,而不需要與用戶進行太多交互;
例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序;
(3)、覆蓋區(Footprint)
在達到前面兩個目標的情況下,盡量減少堆的內存空間,以獲得更好的空間局部性;
可以減少到不滿足前兩個目標為止,然后再解決未滿足的目標;
如果是動態收縮的堆設置,堆的大小將隨着垃圾收集器試圖滿足競爭目標而振盪;
總結就是:低停頓、高吞吐量、少用內存資源;
一般這些目標都相互影響的,增大堆內存獲得高吞吐量但會增長停頓時間,反之亦然,有時需折中處理。
3-2、JVM自適應調整(Ergonomics)
JVM有自適應選擇、調整相關設置的功能;
一般都會先根據平台性能來選擇好垃圾收集器,以及設置好其參數;
在運行中,一些收集器還會收集監控信息來自動地、動態的調整垃圾回收策略;
所以當我們不知道何如選擇收集器和調整時,應該首先讓JVM自適應調整;
然后通過輸出GC日志進行分析,看能不能滿足明確期望的目標(第一步);
如果不能滿足,或者通過打印設置的參數信息,發現可以有更好的調優時,可以進行手動指定參數進行設置,並測試;
3-3、實踐調優:選擇垃圾收集器,並進行相關設置
需要明確一個觀點:
沒有最好的收集器,更沒有萬能的收集;
選擇的只能是對具體應用最適合的收集器;
我們知道HotSpot有這些組合可以搭配使用:
Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;
到實踐調優階段,那必須要了解每個具體收集器的行為特點、優勢和劣勢、調節參數等(請參考前面的文章內容);
然后根據明確期望的目標,選擇具體應用最適合的收集器;
當選擇使用某種並行垃圾收集器時,應該指定期望的具體目標而不是指定堆的大小;
讓垃圾收集器自動地、動態的調整堆的大小來滿足期望的行為;
即堆的大小將隨着垃圾收集器試圖滿足競爭目標而振盪;
當然有時發現問題,堆的大小、划分也是需要進行一些調整的,一般規則:
除非應用程序無法接受長時間的暫停,否則可以將堆調的盡可能大一些;
除非發現問題的原因在於老年代的垃圾收集或應用程序暫停次數過多,否則你應該將堆的較大部分分給年輕代;
等等…
例如,使用Parallel Scavenge/Parallel Old組合,這是一種值得推薦的方式:
1、只需設置好內存數據大小(如"-Xmx"設置最大堆);
2、然后使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"給JVM設置一個優化目標;
3、那些具體細節參數的調節就由JVM自適應完成;
設置調整后,應該通過在產生環境下進行不斷測試,來分析是否達到我們的目標;
更多"期望的目標和JVM自適應調整"信息請參考:
《垃圾收集調優指南》 2節 Ergonomics:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/ergonomics.html#ergonomics
更多"垃圾收集器選擇"信息請參考:
《垃圾收集調優指南》 5節 Available Collectors:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref27