Java虛擬機垃圾回收:內存分配與回收策略 方法區垃圾回收 以及 JVM垃圾回收的調優方法

 在《Java對象在Java虛擬機中的建立過程》瞭解到對象建立的內存分配,在《Java內存區域 JVM運行時數據區》中瞭解到各數據區有些什麼特色、以及相關參數的調整,在Java虛擬機垃圾回收(一) 基礎中瞭解到如何判斷對象是存活仍是已經死亡?在Java虛擬機垃圾回收(二) 垃圾回收算法瞭解到Java虛擬機垃圾回收的幾種常見算法,在《Java虛擬機垃圾回收(三) 7種垃圾收集器》瞭解到幾種收集器的特色和應用等。html

       下面來了解總結前面的一些內容:主要包括內存分配與回收策略、方法區垃圾回收、以及JVM垃圾回收的調優方法、垃圾收集器選擇。java

一、內存分配與回收策略

      經過在《Java虛擬機垃圾回收(二) 垃圾回收算法》"四、分代收集算法"中,咱們知道目前幾乎全部商業虛擬機的垃圾收集器都採用分代收集算法,對於HotSpot通常的年代內存劃分,以下圖:算法

     對象的內存分配從大致上講:數組

       在堆上分配(JIT編譯優化後可能在棧上分配),主要在新生代的Eden區中分配;安全

       若是啓用了本地線程分配緩衝,將線程優先在TLAB上分配;服務器

       少數狀況下,可能直接分配在老年代中。併發

       分配的細節取決於當前使用哪一種垃圾收集器組合,以及JVM中內存相關參數設置。oracle

       接下來將會講解幾條最廣泛的內存分配規則。框架

1-一、對象優先在Eden分配

      前面文章曾介紹HotSpot虛擬機新生代內存佈局及算法ide

       (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-二、大對象直接進入老年代

       大對象指須要大量連續內存空間的Java對象,如,很長的字符串、數組;

       常常出現大對象容易致使內存還有很多空間就提早觸發GC,以獲取足夠的連續空間來存放它們,因此應該儘可能避免使用建立大對象;

      "-XX:PretenureSizeThreshold":

       能夠設置這個閾值,大於這個參數值的對象直接在老年代分配;

       默認爲0(無效),且只對Serail和ParNew兩款收集器有效;

       若是須要使用該參數,可考慮ParNew+CMS組合。

1-三、長期存活的對象將進入老年代

      JVM給每一個對象定義一個對象年齡計數器,其計算流程以下:

       在Eden中分配的對象,經Minor GC後還存活,就複製移動到Survivor區,年齡爲1;

       然後每經一次Minor GC後還存活,在Survivor區複製移動一次,年齡就增長1歲;

       若是年齡達到必定程度,就晉升到老年代中;

      "-XX:MaxTenuringThreshold":

      設置新生代對象晉升老年代的年齡閾值,默認爲15;

1-四、動態對象年齡斷定

      JVM爲更好適應不一樣程序,不是永遠要求等到MaxTenuringThreshold中設置的年齡;

       若是在Survivor空間中相同年齡的全部對象大小總和大於Survivor空間的一半,大於或等於該年齡的對象就能夠直接進入老年代;

1-五、空間分配擔保

       在前面曾簡單介紹過度配擔保:

       當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,而直接檢查歷次晉升到老年代對象的平均大小;    

二、回收方法區

      在《Java內存區域 JVM運行時數據區》曾介紹過方法區及相關的回收問題,雖然JVM規範規定這個區域能夠不實現垃圾收集,且針對常量池和類型卸載的收回效果不佳,但方法區實現垃圾回收是必要的,下面再來詳細瞭解。

2-一、方法區(永久代)的主要回收對象

一、廢棄常量

       與回收Java堆中對象很是相似;

二、無用的類

       同時知足下面3個條件才能算"無用的類":

       (1)、該類全部實例都已經被回收(即Java椎中不存在該類的任何實例);

       (2)、加載該類的ClassLoader已經被回收,也即經過引導程序加載器加載的類不能被回收;

       (3)、該類對應的java.lang.Class對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法;

2-二、須要注意方法區回收的應用

      在大量使用反射、動態代理、常常動態生成大量類的應用,要注意類的回收;

       如運行時動態生成類的應用:

       一、CGLib在Spring、Hibernate等框架中對類進行加強時會使用;

       二、VM的動態語言也會動態建立類來實現語言的動態性;

       三、另外,JSP(第一次使用編譯爲Java類)、基於OSGi頻繁自定義ClassLoader的應用(同一個類文件,不一樣加載器加載視爲不一樣類)等;

2-三、HotSpot虛擬機的相關調整

一、在JDK7中

       使用永久代(Permanent Generation)實現方法區,這樣就能夠不用專門實現方法區的內存管理,但這容易引發內存溢出問題;

       有規劃放棄永久代而改用Native Memory來實現方法區;

       再也不在Java堆的永久代中生成中分配字符串常量池,而是在Java堆其餘的主要部分(年輕代和老年代)中分配;

       更多請參考:http://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html

二、在JDK8中

      永久代已被刪除,類元數據(Class Metadata)存儲空間在本地內存中分配,並用顯式管理元數據的空間:

       從OS請求空間,而後分紅塊;

       類加載器從它的塊中分配元數據的空間(一個塊被綁定到一個特定的類加載器);

       當爲類加載器卸載類時,它的塊被回收再使用或返回到操做系統;

       元數據使用由mmap分配的空間,而不是由malloc分配的空間;

三、相關參數

      "-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

三、JVM垃圾回收的調優方法

     內存回收與垃圾收集器是影響系統性能、併發能力的主要因素之一,通常都須要進行一些手動的測試、調整優化;

       下面介紹的是一些思路,並不是是具體的參數設置。

3-一、明確指望的目標(關注點)

       首先應該明確咱們的應用程序調整垃圾回收指望的目標(關注點)是什麼?

       在前文曾介紹過一般有這些關注點:

(1)、停頓時間

       GC停頓時間越短就適合須要與用戶交互的程序,良好的響應速度能提高用戶體驗;

       與用戶交互較多的場景,以給用戶帶來較好的體驗;

       如常見WEB、B/S系統的服務器上的應用;

(2)、吞吐量

       吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間);    

       高吞吐量能夠高效率地利用CPU時間,儘快完成運算的任務,主要適合在後臺計算而不須要太多交互的任務;

       應用程序運行在具備多個CPU上,對暫停時間沒有特別高的要求;

       程序主要在後臺進行計算,而不須要與用戶進行太多交互;

       例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序;

(3)、覆蓋區(Footprint)

       在達到前面兩個目標的狀況下,儘可能減小堆的內存空間,以得到更好的空間局部性;

       能夠減小到不知足前兩個目標爲止,而後再解決未知足的目標;

       若是是動態收縮的堆設置,堆的大小將隨着垃圾收集器試圖知足競爭目標而振盪;

      總結就是:低停頓、高吞吐量、少用內存資源;

        通常這些目標都相互影響的,增大堆內存得到高吞吐量但會增加停頓時間,反之亦然,有時需折中處理。

3-二、JVM自適應調整(Ergonomics)

       JVM有自適應選擇、調整相關設置的功能;

       通常都會先根據平臺性能來選擇好垃圾收集器,以及設置好其參數;

       在運行中,一些收集器還會收集監控信息來自動地、動態的調整垃圾回收策略;

        

       因此當咱們不知道何如選擇收集器和調整時,應該首先讓JVM自適應調整;

       而後經過輸出GC日誌進行分析,看能不能知足明確指望的目標(第一步);

        

       若是不能知足,或者經過打印設置的參數信息,發現能夠有更好的調優時,能夠進行手動指定參數進行設置,並測試;

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組合,這是一種值得推薦的方式:

       一、只需設置好內存數據大小(如"-Xmx"設置最大堆);

       二、而後使用"-XX:MaxGCPauseMillis"或"-XX:GCTimeRatio"給JVM設置一個優化目標;

       三、那些具體細節參數的調節就由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    

相關文章
相關標籤/搜索