1. 程式人生 > 實用技巧 >JVM 09.3 執行時資料區 堆 調優/垃圾回收/小結

JVM 09.3 執行時資料區 堆 調優/垃圾回收/小結

版權宣告:源出處:尚矽谷JVM

部落格來源於大佬整理

常用調優工具

1.JDK命令列

2.Eclipse:Memory Analyzer Tool

3.Jconsole

4.VisualVM

5.Jprofiler

6.Java Flight Recorder

7.GCViewer

8.GC Easy

Minor GC、Major GC、Full GC

JVM在進行GC時,並非每次都針對上面三個記憶體區域(新生代、老年代、方法區)一起回收的,大部分時候回收都是指新生代。

針對hotSpot VM的實現,它裡面的GC按照回收區域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)

1.部分收集:不是完整收集整個Java堆的垃圾收集。其中又分為:

  • 新生代收集(Minor GC/Young GC):只是新生代的垃圾收集
  • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集
    • 目前,只有CMS GC會有單獨收集老年代的行為
    • 注意,很多時候Major GC 會和 Full GC混淆使用,需要具體分辨是老年代回收還是整堆回收
  • 混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集。
    • 混合收集不涉及方法區回收,只是新生代,老年代的混合收集。
    • 目前,之後G1 GC會有這種行為。

2.整堆收集(Full GC):收集整個java堆和方法區的垃圾收集。

不同GC的觸發機制

年輕代GC(Minor GC)觸發機制
  • 當年輕代空間不足時,就會觸發Minor GC,這裡的年輕代滿指的是Eden代滿,Survivor滿不會引發GC.(每次Minor GC會清理年輕代的記憶體,Survivor是被動GC,不會主動GC)
  • 因為Java隊形大多都具備朝生夕滅的特性,所以Monor GC 非常頻繁,一般回收速度也比較快,這一定義既清晰又利於理解。
  • Minor GC 會引發STW(Stop the World),暫停其他使用者的執行緒,等垃圾回收結束,使用者執行緒才恢復執行。
老年代GC(Major GC/Full GC)觸發機制
  • 指發生在老年代的GC,物件從老年代消失時,Major GC 或者 Full GC 發生了。
  • 出現了Major GC,經常會伴隨至少一次的Minor GC(不是絕對的,在Parallel Scavenge 收集器的收集策略裡就有直接進行Major GC的策略選擇過程)
    • 也就是老年代空間不足時,會先嚐試觸發Minor GC。如果之後空間還不足,則觸發Major GC
  • Major GC速度一般會比Minor GC慢10倍以上,STW時間更長
  • 如果Major GC後,記憶體還不足,就報OOM了
Full GC觸發機制
  • 觸發Full GC執行的情況有以下五種
    • ①呼叫System.gc()時,系統建議執行Full GC,但是不必然執行
    • ②老年代空間不足
    • ③方法區空間不足
    • ④通過Minor GC後進入老年代的平均大小小於老年代的可用記憶體
    • ⑤由Eden區,Survivor S0(from)區向S1(to)區複製時,物件大小由於To Space可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件大小
  • 說明:Full GC 是開發或調優中儘量要避免的,這樣暫停時間會短一些

堆空間分代思想

為什麼要把Java堆分代?不分代就不能正常工作了麼

  • 經研究,不同物件的生命週期不同。70%-99%的物件都是臨時物件。
    • 新生代:有Eden、Survivor構成(s0,s1 又稱為from to),to總為空
    • 老年代:存放新生代中經歷多次依然存活的物件
  • 其實不分代完全可以,分代的唯一理由就是優化GC效能。如果沒有分代,那所有的物件都在一塊,就如同把一個學校的人都關在一個教室。GC的時候要找到哪些物件沒用,這樣就會對堆的所有區域進行掃描,而很多物件都是朝生夕死的,如果分代的話,把新建立的物件放到某一地方,當GC的時候先把這塊儲存“朝生夕死”物件的區域進行回收,這樣就會騰出很大的空間出來。

記憶體分配策略總結

  • 如果物件在Eden出生並經過第一次Minor GC後依然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,把那個將物件年齡設為1.物件在Survivor區中每熬過一次MinorGC,年齡就增加一歲,當它的年齡增加到一定程度(預設15歲,其實每個JVM、每個GC都有所不同)時,就會被晉升到老年代中
    • 物件晉升老年代的年齡閾值,可以通過選項 -XX:MaxTenuringThreshold來設定
  • 針對不同年齡段的物件分配原則如下:
    • 優先分配到Eden
    • 大物件直接分配到老年代
      • 我們要儘量避免程式中出現過多的大物件
    • 長期存活的物件分配到老年代
    • 動態物件年齡判斷
      • 如果Survivor區中相同年齡的所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件可以直接進入到老年代。無需等到MaxTenuringThreshold中要求的年齡

大物件直接進入老年代舉例

分配60m堆空間,新生代 20m ,Eden 16m, s0 2m, s1 2m,buffer物件20m,Eden 區無法存放buffer, 直接晉升老年代。

/** 測試:大物件直接進入老年代
 * -Xms60m -Xmx60m -XX:NewRatio=2 -XX:SurvivorRatio=8 -XX:+PrintGCDetails
 */
public class YoungOldAreaTest {
    // 新生代 20m ,Eden 16m, s0 2m, s1 2m
    // 老年代 40m
    public static void main(String[] args) {
        //Eden 區無法存放buffer  晉升老年代
        byte[] buffer = new byte[1024 * 1024 * 20];//20m
    }
}

日誌輸出:

空間分配擔保機制

簡單解釋一下為什麼會出現這種情況:因為給 allocation2 分配記憶體的時候 eden 區記憶體幾乎已經被分配完了,我們剛剛講了當 Eden 區沒有足夠空間進行分配時,虛擬機器將發起一次 Minor GC.GC 期間虛擬機器又發現 allocation1 無法存入 Survivor 空間,所以只好通過分配擔保機制:把新生代的物件提前轉移到老年代中去,老年代上的空間足夠存放 allocation1,所以不會出現 Full GC。執行 Minor GC 後,後面分配的物件如果能夠存在 eden 區的話,還是會在 eden 區分配記憶體。可以執行如下程式碼驗證:

public class GCTest {

    public static void main(String[] args) {
        byte[] allocation1, allocation2,allocation3,allocation4,allocation5;
        allocation1 = new byte[32000*1024];
        allocation2 = new byte[1000*1024];
        allocation3 = new byte[1000*1024];
        allocation4 = new byte[1000*1024];
        allocation5 = new byte[1000*1024];
    }
}