Java虛擬機——內存管理與垃圾回收

1、Java虛擬機運行時的數據區

image

2、常用的內存區域調節參數

-Xms:初始堆大小,默認爲物理內存的1/64(<1GB);默認(MinHeapFreeRatio參數可以調整)空餘堆內存小於40%時,JVM就會增大堆直到-Xmx的最大限制

-Xmx:最大堆大小,默認(MaxHeapFreeRatio參數可以調整)空餘堆內存大於70%時,JVM會減少堆直到 -Xms的最小限制

-Xmn:新生代的內存空間大小,注意:此處的大小是(eden+ 2 survivor space)。與jmap -heap中顯示的New gen是不同的。整個堆大小=新生代大小 + 老生代大小 + 永久代大小。 
在保證堆大小不變的情況下,增大新生代後,將會減小老生代大小。此值對系統性能影響較大,Sun官方推薦配置爲整個堆的3/8。

-XX:SurvivorRatio:新生代中Eden區域與Survivor區域的容量比值,默認值爲8。兩個Survivor區與一個Eden區的比值爲2:8,一個Survivor區佔整個年輕代的1/10。

-Xss:每個線程的堆棧大小。JDK5.0以後每個線程堆棧大小爲1M,以前每個線程堆棧大小爲256K。應根據應用的線程所需內存大小進行適當調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有限制的,不能無限生成,經驗值在3000~5000左右。一般小的應用, 如果棧不是很深, 應該是128k夠用的,大的應用建議使用256k。這個選項對性能影響比較大,需要嚴格的測試。和threadstacksize選項解釋很類似,官方文檔似乎沒有解釋,在論壇中有這樣一句話:"-Xss is translated in a VM flag named ThreadStackSize”一般設置這個值就可以了。

-XX:PermSize:設置永久代(perm gen)初始值。默認值爲物理內存的1/64。

-XX:MaxPermSize:設置持久代最大值。物理內存的1/4。

3、內存分配方法

1)堆上分配   2)棧上分配  3)堆外分配(DirectByteBuffer或直接使用Unsafe.allocateMemory,但不推薦這種方式)

4、監控方法

1)系統程序運行時可通過jstat –gcutil來查看堆中各個內存區域的變化以及GC的工作狀態; 
2)啓動時可添加-XX:+PrintGCDetails  –Xloggc:&lt;file>輸出到日誌文件來查看GC的狀況; 
3)jmap –heap可用於查看各個內存空間的大小;

5)斷代法可用GC彙總

image

一、新生代可用GC

1)串行GC(Serial Copying):client模式下默認GC方式,也可通過-XX:+UseSerialGC來強制指定;默認情況下 eden、s0、s1的大小通過-XX:SurvivorRatio來控制,默認爲8,含義 
爲eden:s0的比例,啓動後可通過jmap –heap [pid]來查看。

      默認情況下,僅在TLAB或eden上分配,只有兩種情況下會在老生代分配: 
      1、需要分配的內存大小超過eden space大小; 
      2、在配置了PretenureSizeThreshold的情況下,對象大小大於此值。

      默認情況下,觸發Minor GC時: 
      之前Minor GC晉級到old的平均大小 < 老生代的剩餘空間 &lt; eden+from Survivor的使用空間。當HandlePromotionFailure爲true,則僅觸發minor gc;如爲false,則觸發full GC。

      默認情況下,新生代對象晉升到老生代的規則:

     1、經歷多次minor gc仍存活的對象,可通過以下參數來控制:以MaxTenuringThreshold值爲準,默認爲15。 
     2、to space放不下的,直接放入老生代;

2)並行GC(ParNew):CMS GC時默認採用,也可採用-XX:+UseParNewGC強制指定;垃圾回收的時候採用多線程的方式。

3)並行回收GC(Parallel Scavenge):server模式下默認的GC方式,也可採用-XX:+UseParallelGC強制指定;eden、s0、s1的大小可通過-XX:SurvivorRatio來控制,但默認情況下 
以-XX:InitialSurivivorRatio爲準,此值默認爲8,代表的爲新生代大小 : s0,這點要特別注意。

      默認情況下,當TLAB、eden上分配都失敗時,判斷需要分配的內存大小是否 >= eden space的一半大小,如是就直接在老生代上分配;

      默認情況下的垃圾回收規則:

      1、在回收前PS GC會先檢測之前每次PS GC時,晉升到老生代的平均大小是否大於老生代的剩餘空間,如大於則直接觸發full GC; 
      2、在回收後,也會按照上面的規則進行檢測。

      默認情況下的新生代對象晉升到老生代的規則: 
     1、經歷多次minor gc仍存活的對象,可通過以下參數來控制:AlwaysTenure,默認false,表示只要minor GC時存活,就晉升到老生代;NeverTenure,默認false,表示永不晉升到老生代;上面兩個都沒設置的情冴下,如UseAdaptiveSizePolicy,啓動時以InitialTenuringThreshold值作爲存活次數的閾值,在每次ps gc後會動態調整,如不使用UseAdaptiveSizePolicy,則以MaxTenuringThreshold爲準。 
     2、to space放不下的,直接放入老生代。

     在回收後,如UseAdaptiveSizePolicy,PS GC會根據運行狀態動態調整eden、to以及TenuringThreshold的大小。如果不希望動態調整可設置-XX:-UseAdaptiveSizePolicy。如希望跟蹤每次的變化情況,可在啓勱參數上增加: PrintAdaptiveSizePolicy。

二、老生代可用GC

1、串行GC(Serial Copying):client方式下默認GC方式,可通過-XX:+UseSerialGC強制指定。

    觸發機制彙總: 
   1)old gen空間不足; 
   2)perm gen空間不足; 
   3)minor gc時的悲觀策略; 
   4)minor GC後在eden上分配內存仍然失敗; 
   5)執行heap dump時; 
   6)外部調用System.gc,可通過-XX:+DisableExplicitGC來禁止。

2、並行回收GC(Parallel Scavenge): server模式下默認GC方式,可通過-XX:+UseParallelGC強制指定; 並行的線程數爲當cpu core<=8 ? cpu core : 3+(cpu core*5)/8或通過-XX:ParallelGCThreads=x來強制指定。如ScavengeBeforeFullGC爲true(默認值),則先執行minor GC。

3、並行Compacting:可通過-XX:+UseParallelOldGC強制指定。

4、併發CMS:可通過-XX:+UseConcMarkSweepGC來強制指定。併發的線程數默認爲:( 並行GC線程數+3)/4,也可通過ParallelCMSThreads指定。

    觸發機制: 
    1、當老生代空間的使用到達一定比率時觸發;

     Hotspot V 1.6中默認爲65%,可通過PrintCMSInitiationStatistics(此參數在V 1.5中不能用)來查看這個值到底是多少;可通過CMSInitiatingOccupancyFraction來強制指定,默認值並不是賦值在了這個值上,是根據如下公式計算出來的: ((100 - MinHeapFreeRatio) +(double)(CMSTriggerRatio * MinHeapFreeRatio) / 100.0)/ 100.0; 其中,MinHeapFreeRatio默認值: 40   CMSTriggerRatio默認值: 80。

     2、當perm gen採用CMS收集且空間使用到一定比率時觸發;

     perm gen採用CMS收集需設置:-XX:+CMSClassUnloadingEnabled   Hotspot V 1.6中默認爲65%;可通過CMSInitiatingPermOccupancyFraction來強制指定,同樣,它是根據如下公式計算出來的:((100 - MinHeapFreeRatio) +(double)(CMSTriggerPermRatio* MinHeapFreeRatio) / 100.0)/ 100.0; 其中,MinHeapFreeRatio默認值: 40    CMSTriggerPermRatio默認值: 80。

      3、Hotspot根據成本計算決定是否需要執行CMS GC;可通過-XX:+UseCMSInitiatingOccupancyOnly來去掉這個動態執行的策略。 
      4、外部調用了System.gc,且設置了ExplicitGCInvokesConcurrent;需要注意,在hotspot 6中,在這種情況下如應用同時使用了NIO,可能會出現bug。

6、GC組合

1)默認GC組合

image

2)可選的GC組合

image

7、GC監測

1)jstat –gcutil [pid] [intervel] [count] 
2)-verbose:gc // 可以輔助輸出一些詳細的GC信息;-XX:+PrintGCDetails // 輸出GC詳細信息;-XX:+PrintGCApplicationStoppedTime // 輸出GC造成應用暫停的時間 
-XX:+PrintGCDateStamps // GC發生的時間信息;-XX:+PrintHeapAtGC // 在GC前後輸出堆中各個區域的大小;-Xloggc:[file] // 將GC信息輸出到單獨的文件中,建議都加上,這個消耗不大,而且對查問題和調優有很大的幫助。gc的日誌拿下來後可使用GCLogViewer或gchisto進行分析。 
3)圖形化的情況下可直接用jvisualvm進行分析。

4)查看內存的消耗狀況

      (1)長期消耗,可以直接dump,然後MAT(內存分析工具)查看即可

      (2)短期消耗,圖形界面情況下,可使用jvisualvm的memory profiler或jprofiler。

8、系統調優方法

步驟:1、評估現狀 2、設定目標 3、嘗試調優 4、衡量調優 5、細微調整

設定目標:

1)降低Full GC的執行頻率? 
2)降低Full GC的消耗時間? 
3)降低Full GC所造成的應用停頓時間? 
4)降低Minor GC執行頻率? 
5)降低Minor GC消耗時間? 
例如某系統的GC調優目標:降低Full GC執行頻率的同時,儘可能降低minor GC的執行頻率、消耗時間以及GC對應用造成的停頓時間。

衡量調優:

1、衡量工具 
1)打印GC日誌信息:-XX:+PrintGCDetails –XX:+PrintGCApplicationStoppedTime -Xloggc: {文件名}  -XX:+PrintGCTimeStamps 
2)jmap:(由於每個版本jvm的默認值可能會有改變,建議還是用jmap首先觀察下目前每個代的內存大小、GC方式) ? 
3)運行狀況監測工具:jstat、jvisualvm、sar 、gclogviewer

2、應收集的信息 
1)minor gc的執行頻率;full gc的執行頻率,每次GC耗時多少? 
2)高峯期什麼狀況? 
3)minor gc回收的效果如何?survivor的消耗狀況如何,每次有多少對象會進入老生代? 
4)full gc回收的效果如何?(簡單的memory leak判斷方法) 
5)系統的load、cpu消耗、qps or tps、響應時間

QPS每秒查詢率:是對一個特定的查詢服務器在規定時間內所處理流量多少的衡量標準。在因特網上,作爲域名服務器的機器性能經常用每秒查詢率來衡量。對應fetches/sec,即每秒的響應請求數,也即是最大吞吐能力。 
TPS(Transaction Per Second):每秒鐘系統能夠處理的交易或事務的數量。

嘗試調優:

注意Java RMI的定時GC觸發機制,可通過:-XX:+DisableExplicitGC來禁止或通過 -Dsun.rmi.dgc.server.gcInterval=3600000來控制觸發的時間。

1)降低Full GC執行頻率 – 通常瓶頸 
老生代本身佔用的內存空間就一直偏高,所以只要稍微放點對象到老生代,就full GC了; 
通常原因:系統緩存的東西太多; 
例如:使用oracle 10g驅動時preparedstatement cache太大; 
查找辦法:現執行Dump然後再進行MAT分析;

(1)Minor GC後總是有對象不斷的進入老生代,導致老生代不斷的滿 
通常原因:Survivor太小了 
系統表現:系統響應太慢、請求量太大、每次請求分配的內存太多、分配的對象太大... 
查找辦法:分析兩次minor GC之間到底哪些地方分配了內存; 
利用jstat觀察Survivor的消耗狀況,-XX:PrintHeapAtGC,輸出GC前後的詳細信息; 
對於系統響應慢可以採用系統優化,不是GC優化的內容;

(2)老生代的內存佔用一直偏高 
調優方法:① 擴大老生代的大小(減少新生代的大小或調大heap的 大小); 
減少new注意對minor gc的影響並且同時有可能造成full gc還是嚴重; 
調大heap注意full gc的時間的延長,cpu夠強悍嘛,os是32 bit的嗎? 
② 程序優化(去掉一些不必要的緩存)

(3)Minor GC後總是有對象不斷的進入老生代 
前提:這些進入老生代的對象在full GC時大部分都會被回收 
調優方法: 
① 降低Minor GC的執行頻率; 
② 讓對象儘量在Minor GC中就被回收掉:增大Eden區、增大survivor、增大TenuringThreshold;注意這些可能會造成minor gc執行頻繁; 
③ 切換成CMS GC:老生代還沒有滿就回收掉,從而降低Full GC觸發的可能性; 
④ 程序優化:提升響應速度、降低每次請求分配的內存、

(4)降低單次Full GC的執行時間 
通常原因:老生代太大了... 
調優方法:1)是並行GC嗎?   2)升級CPU  3)減小Heap或老生代

(5)降低Minor GC執行頻率 
通常原因:每次請求分配的內存多、請求量大 
通常辦法:1)擴大heap、擴大新生代、擴大eden。注意點:降低每次請求分配的內存;橫向增加機器的數量分擔請求的數量。

(6)降低Minor GC執行時間 
通常原因:新生代太大了,響應速度太慢了,導致每次Minor GC時存活的對象多 
通常辦法:1)減小點新生代吧;2)增加CPU的數量、升級CPU的配置;加快系統的響應速度

細微調整:

首先需要了解以下情況:

① 當響應速度下降到多少或請求量上漲到多少時,系統會宕掉?

② 參數調整後系統多久會執行一次Minor GC,多久會執行一次Full GC,高峯期會如何?

需要計算的量:

①每次請求平均需要分配多少內存?系統的平均響應時間是多少呢?請求量是多少、多常時間執行一次Minor GC、Full GC?

②現有參數下,應該是多久一次Minor GC、Full GC,對比真實狀況,做一定的調整;

必殺技:提升響應速度、降低每次請求分配的內存?

9、系統調優舉例

     現象:1、系統響應速度大概爲100ms;2、當系統QPS增長到40時,機器每隔5秒就執行一次minor gc,每隔3分鐘就執行一次full gc,並且很快就一直full GC了;4、每次Full gc後舊生代大概會消耗400M,有點多了。

     解決方案:解決Full GC次數過多的問題

    (1)降低響應時間或請求次數,這個需要重構,比較麻煩;——這個是終極方法,往往能夠順利的解決問題,因爲大部分的問題均是由程序自身造成的。

    (2)減少老生代內存的消耗,比較靠譜;——可以通過分析Dump文件(jmap dump),並利用MAT查找內存消耗的原因,從而發現程序中造成老生代內存消耗的原因。

    (3)減少每次請求的內存的消耗,貌似比較靠譜;——這個是海市蜃樓,沒有太好的辦法。

    (4)降低GC造成的應用暫停的時間——可以採用CMS GS垃圾回收器。參數設置如下:

     -Xms1536m -Xmx1536m -Xmn700m -XX:SurvivorRatio=7 -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection

     -XX:CMSMaxAbortablePrecleanTime=1000 -XX:+CMSClassUnloadingEnabled -XX:+UseCMSInitiatingOccupancyOnly -XX:+DisableExplicitGC

    (5)減少每次minor gc晉升到old的對象。可選方法:1) 調大新生代。2)調大Survivor。3)調大TenuringThreshold。

      調大Survivor:當前採用PS GC,Survivor space會被動態調整。由於調整幅度很小,導致了經常有對象直接轉移到了老生代;於是禁止Survivor區的動態調整了,-XX:-UseAdaptiveSizePolicy,並計算Survivor Space需要的大小,於是繼續觀察,並做微調…。最終將Full GC推遲到2小時1次。

10、垃圾回收的實現原理

      內存回收的實現方法:1)引用計數:不適合複雜對象的引用關係,尤其是循環依賴的場景。2)有向圖Tracing:適合於複雜對象的引用關係場景,Hotspot採用這種。常用算法:Copying、Mark-Sweep、Mark-Compact。

      Hotspot從root set開始掃描有引用的對象並對Reference類型的對象進行特殊處理。 
      以下是Root Set的列表:1)當前正在執行的線程;2)全局/靜態變量;3)JVM Handles;4)JNI 【 Java Native Interface 】Handles;

      另外:minor GC只掃描新生代,當老生代的對象引用了新生代的對象時,會採用如下的處理方式:在給對象賦引用時,會經過一個write barrier的過程,以便檢查是否有老生代引用新生代對象的情況,如有則記錄到remember set中。並在minor gc時,remember set指向的新生代對象也作爲root set。

     新生代串行GC(Serial Copying):

     新生代串行GC(Serial Copying)完整內存的分配策略:

     1)首先在TLAB(本地線程分配緩衝區)上嘗試分配; 
     2)檢查是否需要在新生代上分配,如需要分配的大小小於PretenureSizeThreshold,則在eden區上進行分配,分配成功則返回;分配失敗則繼續; 
     3)檢查是否需要嘗試在老生代上分配,如需要,則遍歷所有代並檢查是否可在該代上分配,如可以則進行分配;如不需要在老生代上嘗試分配,則繼續; 
     4)根據策略決定執行新生代GC或Full GC,執行full gc時不清除soft Ref; 
     5)如需要分配的大小大於PretenureSizeThreshold,嘗試在老生代上分配,否則嘗試在新生代上分配; 
     6)嘗試擴大堆並分配; 
     7)執行full gc,並清除所有soft Ref,按步驟5繼續嘗試分配。  

     新生代串行GC(Serial Copying)完整內存回收策略 
     1)檢查to是否爲空,不爲空返回false; 
     2)檢查老生代剩餘空間是否大於當前eden+from已用的大小,如大於則返回true,如小於且HandlePromotionFailure爲true,則檢查剩餘空間是否大於之前每次minor gc晉級到老生代的平均大小,如大於返回true,如小於返回false。 
     3)如上面的結果爲false,則執行full gc;如上面的結果爲true,執行下面的步驟; 
     4)掃描引用關係,將活的對象copy到to space,如對象在minor gc中的存活次數超過tenuring_threshold或分配失敗,則往老生代複製,如仍然複製失敗,則取決於HandlePromotionFailure,如不需要處理,直接拋出OOM,並退出vm,如需處理,則保持這些新生代對象不動;

    新生代可用GC-PS

    完整內存分配策略 
    1)先在TLAB上分配,分配失敗則直接在eden上分配; 
    2)當eden上分配失敗時,檢查需要分配的大小是否 >= eden space的一半,如是,則直接在老生代分配; 
    3)如分配仍然失敗,且gc已超過頻率,則拋出OOM; 
    4)進入基本分配策略失敗的模式; 
    5)執行PS GC,在eden上分配; 
    6)執行非最大壓縮的full gc,在eden上分配; 
    7)在舊生代上分配; 
    8)執行最大壓縮full gc,在eden上分配; 
    9)在舊生代上分配; 
    10)如還失敗,回到2。

   最悲慘的情況,分配觸發多次PS GC和多次Full GC,直到OOM。

   完整內存回收策略 
   1)如gc所執行的時間超過,直接結束; 
   2)先調用invoke_nopolicy 
       2.1 先檢查是不是要嘗試scavenge; 
       2.1.1 to space必須爲空,如不爲空,則返回false; 
       2.1.2 獲取之前所有minor gc晉級到old的平均大小,並對比目前eden+from已使用的大小,取更小的一個值,如老生代剩餘空間小於此值,則返回false,如大於則返回true; 
       2.2 如不需要嘗試scavenge,則返回false,否則繼續; 
       2.3 多線程掃描活的對象,並基亍copying算法回收,回收時相應的晉升對象到舊生代; 
       2.4 如UseAdaptiveSizePolicy,那麼重新計算to space和tenuringThreshold的值,並調整。 
   3)如invoke_nopolicy返回的是false,或之前所有minor gc晉級到老生代的平均大小 &gt; 舊生代的剩餘空間,那麼繼續下面的步驟,否則結束; 
   4)如UseParallelOldGC,則執行PSParallelCompact,如不是UseParallelOldGC,則執行PSMarkSweep。

    老生代並行CMS GC:

    優缺點:

    1) 大部分時候和應用併發進行,因此只會造成很短的暫停時間; 
    2)浮動垃圾,沒辦法,所以內存空間要稍微大一點; 
    3)內存碎片,-XX:+UseCMSCompactAtFullCollection 來解決; 
    4) 爭搶CPU,這GC方式就這樣; 
    5)多次remark,所以總的gc時間會比並行的長; 
    6)內存分配,free list方式,so性能稍差,對minor GC會有一點影響; 
    7)和應用併發,有可能分配和回收同時,產生競爭,引入了鎖,JVM分配優先。

11、TLAB的解釋

     堆內的對象數據是各個線程所共享的,所以當在堆內創建新的對象時,就需要進行鎖操作。鎖操作是比較耗時,因此JVM爲每個線在堆上分配了一塊“自留地”——TLAB(全稱是Thread Local Allocation Buffer),位於堆內存的新生代,也就是Eden區。每個線程在創建新的對象時,會首先嚐試在自己的TLAB裏進行分配,如果成功就返回,失敗了再到共享的Eden區裏去申請空間。在線程自己的TLAB區域創建對象失敗一般有兩個原因:一是對象太大,二是自己的TLAB區剩餘空間不夠。通常默認的TLAB區域大小是Eden區域的1%,當然也可以手工進行調整,對應的JVM參數是-XX:TLABWasteTargetPercent。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章