JVM的內存管理和垃圾回收

jopen 10年前發布 | 102K 次閱讀 JVM Java開發

   文章參考了幾篇博文,但由于原博文都存在一點點問題,因此自己寫一篇總結,原博文在結尾給出。歡迎就jvm提出自己的疑問,共同探討學習。

   本文主要是基于Sun JDK 1.6 Garbage Collector(作者:畢玄)的整理與總結,ppt下載地址:to-do

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

                

                              (圖片來自深入java虛擬機2.2節)

詳細一點的關系:

備注:在Hotspot中本地方法棧和JVM方法棧是同一個,因此也可用-Xss控制

1)、程序計數器是一塊較小的內存空間,它的作用可以看作是當前線程所執行的字節碼或行號指示器。此區域沒有OutOfMemoryError.

2)Java虛擬機棧是線程私有的,它的生命周期與線程相同。每個線程執行每個方法的時候都會在棧中申請一個棧幀,每個棧幀包括局部變量區和操作數棧,用于存放此次方法調用過程中的臨時變量、參數和中間結果

3)、本地方法棧是用于支持native方法的執行,存儲了每個native方法調用的狀態,為虛擬機使用到的native方法服務

4)、方法區:它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。會出現OutOfMemoryError。當增強的類越多,就需要越大的方法區來保證動態生成的class可以加載到內存(CGLIB等都是通過動態生成字節碼來生成class)。HotSpot虛擬機使用永久代來實現方法區,但兩者并不等價(對于其他實現來說)。JVM用持久代(Permanet Generation)來存放方法區,可通過-XX:PermSize和-XX:MaxPermSize來指定最小值和最大值

5)、堆:所有通過new創建的對象的內存都在堆中分配,其大小可以通過-Xmx和-Xms來控制。堆被劃分為新生代和舊生代,新生代又被進一步劃分為Eden和Survivor區,最后Survivor由From Space和To Space組成,結構圖如下所示:

新生代(New Generation)使用-Xmn進行設置,而Eden和S0的比例使用-XX:SurvivorRatio進行設置,S0和S1是相等的。新生代區域這么劃分是因為新生代的收集算法為標記復制算法,每次Minor GC時Eden和S0(或S1)都會把標記的對象拷貝到S1(S0),注意S0和S1是等價的,這樣一次Young GC后留下來的對象age加1。

備注:通常將對新生代進行的回收稱為Minor GC;對舊生代進行的回收稱為Major GC,但由于Major GC除并發GC外均需對整個堆進行掃描和回收,因此又稱為Full GC。

老年代:用于存放新生代中經過多次垃圾回收仍然存活的對象,也有可能是新生代分配不了內存的大對象會直接進入老年代。

注:從上圖可以看出,整個堆大小=年輕代大小 + 老年代大小,而永久代是不包括在堆中的。

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:<file>輸出到日志文件來查看GC的狀況; 
3)jmap –heap可用于查看各個內存空間的大小;


5)GC收集器匯總

圖注:連線的代表可以配合使用的收集器

一、新生代可用GC

   1)串行GC(Serial):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的平均大小<老生代的剩余空間<eden+from Survivor的使用空間。當HandlePromotionFailure為true,則僅觸發minor gc;如為false,則觸發full GC。

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

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

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

 3)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):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、Parallel Old:可通過-XX:+UseParallelOldGC強制指定。

3、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。

注意:

1)、參數-XX:+UseParallelGC,使用Parallel Scavenge +  Serial Old的收集器組合進行回收

2)、新生代使用標記復制算法,老年代使用并發標記清除(CMS)或標記-壓縮(內存向一端移動)。

     而收集器類型是  年輕: 串行 并行,老年: 串行 并行 并發 

3)、收集器的并行和并發的區別:并行是GC線程有多個, 但在運行GC線程時 ,用戶線程是阻塞的;

      并發收集器 ,是大部分階段用戶線程和GC線程都在運行,這里說的大部分是因為CMS在初始標記和重新標記階段仍會發生STW(Stop The World)的時候

6、GC組合

1)默認GC組合

image

2)可選的GC組合

image

7、GC監測

1)gc的日志拿下來后可使用GCLogViewer或gchisto進行分析。

jstat –gcutil [pid] [intervel] [count]
-verbose:gc // 可以輔助輸出一些詳細的GC信息;
-XX:+PrintGCDetails // 輸出GC詳細信息;
-XX:+PrintGCApplicationStoppedTime // 輸出GC造成應用暫停的時間
-XX:+PrintGCDateStamps // GC發生的時間信息;
-XX:+PrintHeapAtGC // 在GC前后輸出堆中各個區域的大小;
-Xloggc:[file] // 將GC信息輸出到單獨的文件中,建議都加上,這個消耗不大,而且對查問題和調優有很大的幫助。

2)圖形化的情況下可直接用jvisualvm進行分析。

3)查看內存的消耗狀況

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

   b.短期消耗,圖形界面情況下,可使用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晉級到老生代的平均大小 > 舊生代的剩余空間,那么繼續下面的步驟,否則結束;
   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。

參考文獻:

1、Sun JDK 1.6 GC(Garbage Collector)  作者:畢玄

2、原博文地址:http://www.blogjava.net/chhbjh/archive/2012/01/28/368936.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!