1. 程式人生 > >GC學習入門 看這一篇就夠了(基於Oracle JDK 8)

GC學習入門 看這一篇就夠了(基於Oracle JDK 8)

目錄

垃圾收集 (Garbage Collection) 機制是 Java 的一大優勢特性, 為充分榨取 JVM 效能, 避免系統因垃圾收集不及時導致的 OOM (OutOfMemory, 記憶體溢位)問題, 或記憶體飽和出現無法響應使用者請求的情況, 就需要根據伺服器配置及應用複雜度對 GC 策略進行優化, 以確保系統正常執行.

1 JVM 中 Java 物件的分類

JVM根據運行於其中的物件的生存時間, 將它們分為3種, 並分別存放在JVM的不同記憶體區域中. 這種物件存放空間的管理方式叫做 Generation管理方式.

  1. Young Generation (新生代, 又稱年輕代): 用於存放"早逝"物件(即瞬時物件), 一般的 Java 應用中, 80%的物件都是"朝生息滅"的, 比如在建立物件或呼叫方法時使用的臨時物件或區域性變數.
  2. Tenured Generation (老年代): 用於存放"駐留"物件(即被引用較長時間的物件). 往往體現為一個大型程式中的全域性物件或長時間被使用的物件.
  3. Perm Generation (永久代): 用於存放"永久"物件. 這些物件管理著運行於 JVM 中的類和方法.

2 JVM 的 GC 型別及觸發條件

2.1 Young GC

又叫 Minor GC(次收集), Young GC 經常發生, 且其每次消耗的時間較短 —— 它只對Young Generation 中的物件進行垃圾收集.

  • 觸發條件:

    在 Young Generation(新生代)的 Edne 區的空間不足以容納新生成的物件時執行, 同時會將 Eden 區與 From Survivor 區中尚且存活的物件移動至空閒的 To Survivor 區中.

    —— 程式執行過程中, 始終有一個 Survivor 區是完全處於空閒狀態的, 如果不是, 說明應用程式出現故障了.

2.2 Full GC

又叫 Major GC(主收集), 是對整個 Java Heap 中的物件(不包括永久代/元空間)進行垃圾收集, 此 GC 操作耗時久, 對系統的效能影響較大, 因此在 JVM 的調優中, 很多工作是針對 Full GC 的調優 —— 要儘可能減少 Full GC 的頻率.

Full GC 是一種"昂貴"的垃圾收集方式, 它要對整個Heap 進行垃圾收集, 並做一定的空間整理, 這會使 Stop-The-World 的時間變長.

  • Full GC 的觸發條件:

1) 年老代(Tenured)空間不足:

  • 通過 Minor GC 後進入老年代的物件的體積大於老年代的可用空間;
  • 由Eden塊、From Space 塊向 To Space 複製存活物件時, 它們的體積大於 To Space 的大小, 系統就會把這些物件轉存到老年代, 而老年代的可用空間小於這些物件的體積.

2) System.gc() 被顯式呼叫, 系統建議執行 Full GC, 但並不會立即執行 —— 非常影響程式效能, 建議禁止使用;

3) 上一次 GC 之後 Heap 各個區域空間的動態變化.

3 Java 物件生成時的記憶體申請過程

1) JVM 會試圖為相關 Java 物件在年輕代的 Eden 區中初始化一塊記憶體區域;

2) 當 Eden 區空間足夠時, 記憶體申請結束. 否則執行下一步;

3) JVM 試圖釋放在 Eden 區中所有不活躍的物件(即 出發Young GC), 釋放後若Eden空間仍然不足以放入新物件時, JVM 會試圖將部分 Eden 區中活躍的物件遷移至 Survivor 區;

4) Survivor 區被用來作為 Eden 區及老年代的中間交換區域, 當老年代空間足夠時, Survivor 區中存活了一定次數的物件會被遷移到老年代;

5) 當年老代空間不夠時, JVM會在老年代進行完全的垃圾回收(Full GC);

6) Full GC 後, 若 Survivor 區及老年代仍然無法存放從 Eden 區複製過來的物件, 則會導致 JVM 無法在 Eden 區為新生成的物件申請記憶體, 即出現 "Out of Memory".

  • OOM(Out of Memory)異常一般主要有如下2種原因:

    1) 老年代溢位, 表現為: java.lang.OutOfMemoryError:Javaheapspace

    這是最常見的情況, 產生的原因可能是: 設定的記憶體引數-Xmx過小或程式的記憶體洩露及使用不當問題.

    2) 持久代溢位,表現為: java.lang.OutOfMemoryError:PermGenspace

    通常由於持久代設定過小, 動態載入了大量 Java 類而導致溢位, 解決辦法唯有將引數 -XX:MaxPermSize 調大(一般256m能滿足絕大多數應用程式需求).

3 Oracle JDK 中的垃圾收集器

3.1 序列收集器(Serial Collector)

只有一條GC執行緒, 執行時需要暫停使用者程式(Stop-The-World).

實現: Serial(用於新生代, 採用複製演算法)、serial old(用於老年代, 採用標記-整理演算法).

3.2 並行收集器(Parallel Collector)

有多條GC執行緒, 執行時也需要暫停使用者程式(Stop-The-World).

實現: ParNew(用於新生代, 採用複製演算法)、Parallel Scavenge(用於新生代, 採用複製演算法)、Parallel Old(用於老年代, 採用標記-整理演算法).

3.3 併發收集器(Concurrent Collector)

有一條或多條GC執行緒, 且需要在部分階段暫停使用者程式(Stop-The-World), 部分階段與使用者程式併發執行.

實現: Concurrent Mark Sweep(CMS, 用於老年代, 採用標記-清除演算法).

  • 併發(concurrent)與並行(parallel)的比較:
    1. 併發就是兩個任務(A和B)需要獨立執行, 在任務A結束之前, 任務B開始執行 --- 即表面上多個任務同時執行.
    2. 並行, 類比序列, 是微觀概念, 即在每一個時刻都有多個任務在同時執行, 形象點理解為多管齊下, 序列可理解為單列佇列, 同一時刻只能執行一個任務.
    3. 事實上, 並行是併發的一種實現方式, 還有一種併發的實現方式, 即我們熟悉的時間片切換 --- 任務A執行一段時間, CPU再切換到任務B執行一段時間, 如此交替執行. 時間片切換在微觀上仍然是序列 --- 某一具體時刻只有一個任務在執行, 而在巨集觀上, 即一段時間內, 有多個任務得到了執行.
  • 總結: 並行必須在多核多處理器或分散式系統(本質還是多核多處理器)中才能發生, 而單核處理器上只能發生時間片切換.

3.4 G1 收集器(Garbage First GC)

G1垃圾回收器在 Oracle JDK 7 開始提供完整支援, 它是 server 型的 GC, 主要針對多核處理器和大記憶體的伺服器, 能夠以很高的概率 滿足開發人員對停頓時間的要求, 同時還能保證高吞吐量.

  • 與 CMS 收集器相比, G1收集器的優勢:

    1) 基於標記-整理演算法, 不會產生大量的記憶體碎片;

    2) 可以更加精確地控制停頓時間, 在不犧牲吞吐量前提下, 實現低停頓垃圾回收.

  • G1收集器的實現原理:

    G1收集器能夠避免全區域的垃圾收集, 它把堆記憶體劃分為大小固定的幾個獨立區域, 並跟蹤這些區域的垃圾收集進度, 同時在後臺維護一個優先順序列表, 每次根據所允許的收集時間, 優先回收垃圾最多的區域.

    —— 區域劃分和優先順序區域回收機制, 確保G1收集器可以在有限的時間內獲得最高的垃圾收集效率.

  • G1的長期目標是取代CMS (Concurrent Mark-Sweep Collector) 併發標記-清除收集器.

3.5 其他概念說明

  1. 為了更大程度地榨取機器效能, 新生代的收集器都使用了複製演算法, 老年代的收集器都使用 標記-清除 或 標記-整理 演算法. 關於各演算法詳情, 請參閱: JVM記憶體管理———垃圾蒐集器引數精解.
  2. 在實際應用中, 需要對JVM的新生代、老年代分別選擇合適的垃圾收集器.
  3. 這裡新生代和老年代都分別有三種實現, 但由於收集器的實現方式不同, 部分組合無法一起配合工作, 經過驗證, 這六種垃圾收集器只有六種可用組合.

4 GC 的配置引數

4.1 引數名稱的說明

4.1.1 標準引數(-)

所有 JVM 都必須支援這些引數的功能, 而且向後相容, 如:

  • -client : 設定 JVM 使用 client 模式, 特點是啟動速度比較快, 但執行時效能和記憶體管理效率不高. 通常用於客戶端應用程式或開發除錯; 在32位環境下直接執行 Java 程式預設啟用該模式.

  • -server : 設定 JVM 使 server 模式, 特點是啟動速度比較慢, 但執行時效能和記憶體管理效率很高, 適用於生產環境; 在具有64位能力的JDK環境下預設啟用該模式.

4.1.2 非標準引數(-X)

各 JVM 廠商應該都實現這些引數的功能, 但是並不保證所有的 JVM 實現都滿足, 且不保證向後相容;

4.1.3 非穩定引數(-XX)

此類引數各個JVM實現會有所不同, 將來可能會不被支援, 需要慎重使用.

  • 注意: 在"-XX:"後的引數若不需要賦值, 即只是用來配置開啟或關閉相應選項, 則需要有 "+" (開啟) 或 "-" (禁止) , 否則應用程式將在日誌檔案 (如 Tomcat 的日誌檔案 catalina.out ) 中丟擲如下錯誤:

    Missing +/- setting for VM option 'UseConcMarkSweepGC'. 
    Error: Could not create the Java Virtual Machine.
    Error: A fatal exception has occurred. Program will exit. 

    可以看出, 這裡缺少了 "+/-" 符號, 導致虛擬機器啟動異常, 修改後即可正常啟動.

4.2 序列 GC 引數

-XX:+UseSerialGC 
# 使用 Serial & Serial Old 序列收集器(JDK 5以前的主要收集方式), 是client模式的預設設定. 

4.3 並行 GC 引數(吞吐量優先)

-XX:+UseParNewGC 
# 使用 ParNew & Serial Old 收集器, 即對新生代使用並行收集, 提高收集效率(縮短Young GC的時間), 不推薦. 

-XX:+UseParallelGC 
# 使用 Parallel Scavenge & Parallel Old 並行收集器, 吞吐量優先, 會消耗更多記憶體, 是server模式的預設設定. 

-XX:+UseParallelOldGC 
# 對老年代使用 Parallel Old 並行收集器(JDK 6開始支援). 

-XX:ParallelGCThreads=20 
# 配置並行收集器的執行緒數, 即並行執行垃圾收集任務的執行緒個數. 
# 此值最好與CPU處理器的個數相同(預設即相同).

-XX:GCTimeRatio=49 
# 設定系統用作GC的時間比例, 如49, 則GC時間比為 1/(1+49) = 2%, 即Java用2%的時間來做垃圾收集. 
# 如果此值設定過大, 即GC時間太少導致GC無法完成, JVM會壓縮新生代的大小以適應此配置. 

-XX:MaxGCPauseMillis=100 
# 設定每次新生代垃圾收集的最長毫秒值, 如果時間久而新生代的大小不足以支撐到此時間, 
# JVM會自動調整新生代的大小以滿足此值. 若仍然無法滿足, 則會調整GCTimeRatio. 

-XX:+UseAdaptiveSizePolicy 
# 使並行收集器自動設定 Eden 區的大小與相應的 Survivor 區的比例, 
# 以達到目標系統規定的最低響應時間或收集頻率等. 建議在使用並行收集器時始終開啟此選項. 

4.4 併發 GC 引數(響應時間優先)

-XX:+UseConcMarkSweepGC 
# JDK 5開始提供支援, 以響應時間優先--即縮短Full GC的時間. 
# JVM 會根據系統配置自行設定使用 ParNewGC & CMS(Serial Old作為替補)收集器. 
# 建議在 Heap Size 較大且 Full GC 時間較長, 對響應時間的需求大於對吞吐量的需求, 能夠承受垃圾回收執行緒和應用執行緒共享CPU資源等情況下使用. 

-XX:+UseCMSCompactAtFullCollection  
# CMS是不會移動記憶體的, 此引數設定在每次Full GC後對老年代空間進行壓縮整理, 會影響效能, 但是能減少記憶體碎片. 

-XX:CMSInitiatingOccupancyFraction=70 
# 觸發CMS收集器的記憶體佔用閾值, 預設為90%: 當老年代記憶體空間使用率達到90%時, 就開始對老年代進行CMS併發垃圾收集. 
# 這個引數設定不當, 將發生promotion failed(晉升失敗). 

-XX:CMSFullGCsBeforeCompaction=10 
# 由於併發收集器不對記憶體空間進行壓縮整理, 所以執行一段時間後會產生"碎片", 使得執行效率降低. 此配置項用來設定在幾次GC後觸發一次記憶體整理. 
# 此配置項即將被移除(JDK 8已不建議使用). 

4.5 G1 GC 引數

-XX:+UseG1GC 
# 使用G1收集器

-XX:MaxGCPauseMillis=200    
# 設定回收器的最大停頓毫秒值, 這是一個概率目標, JVM將盡最大努力去實現它. 

-XX:G1ReservePercent=15
# 設定堆的臨時上限, 以防止因堆擴大失敗而導致的異常. 預設值是10. 

-XX:G1HeapRegionSize=16
# 使用 G1 的 Java 堆細分為大小相等的區域(Region), 此選項是設定單個區域的大小, 預設值是基於堆大小的一種人體工效劃分, 最小值是1Mb, 最大值是32Mb.
# 人體工效: 根據平臺相關的預設選擇和根據需求動態垃圾回收的行為統稱為人體工效, 人體工效的作用就是可以通過少量的命令列選項就可以讓JVM提供最合適的效能. 

4.6 通用 GC 引數

-Xnoclassgc 
# 禁用類垃圾收集, 能提高效能. 

-XX:MaxHeapFreeRatio=70 
# GC過後堆的最大空閒空間比例, 避免過渡壓縮

-XX:MinHeapFreeRatio=40 
# GC過後堆的最小空閒空間比例, 避免過度膨脹

-XX:MaxTenuringThreshold=5 
# 晉升老年代的最大年齡, 預設為15: 新生代物件在15次 Minor GC 後將被轉移至老年代. --- 必須在0-15之間. 
# 如果設定為0, 等同於去掉了新生代的空間, 新生代物件將會越過 Survivor 區直接進入老年代, 很快就會佔滿老年代發生Full GC. 
# 同時, 這在老年代物件較多的應用中卻可以提高效率. 
# 如果將此值調大, 則新生代物件會在 Survivor 區進行多次複製, 即增加了物件在新生代的存活時間. 

-XX:PretenureSizeThreshold=10 
# 晉升老年代的物件的大小, 預設為0. 比如設為10M, 則超過10M的物件將越過 Eden區, 直接存入老年代.

-XX:+UseThreadPriorities 
# 啟用本地執行緒優先順序API, 使 java.lang.Thread.setPriority() 生效, 不啟用則無效

-XX:+DisableExplicitGC 
# 禁用寫在程式中的 System.gc(), 即禁止開發人員呼叫 gc() 方法影響效能. 

-XX:+ExplicitGCInvokesConcurrent 
# 配置 System.gc() 可以和應用程式一起併發執行. 
# System.gc() 用來回收不用的記憶體, 此方法只是"建議" JVM 回收記憶體, 不能強制回收, 具體回收時機由 JVM 決定. 

-XX:TargetSurvivorRatio=90 
# 允許90%的 Survivor 區被佔用(JVM預設為50%), 提高對於 Survivor 區的使用率

-XX:SoftRefLRUPolicyMSPerMB=1 
# Soft Reference(弱引用)在虛擬機器中比在客戶機中存活的時間更長, 其清除頻率可用此命令來控制. 
# 此命令用來指定每 MB 堆空間中 Soft Reference 存活的秒數, 預設值為1000毫秒: 物件的最後一個強引用被收集之後, 存活1秒鐘. 
# 這是個近似值: Soft Reference只會在垃圾收集時才會被清除, 而垃圾收集並不總是發生. 可改為0, 客戶機中不使用就立即清除. 

4.7 其他說明

  • 在記憶體調優中, 記憶體設定越大, 處理請求的效率也就越高, 但同時垃圾收集所需要的時間也就越長, 在垃圾收集期間的處理效率肯定會受影響, 因此需要作出相應的權衡.

  • 關於 promotion failed(晉升失敗) : JVM發生Young GC時, Eden區存活的物件 和 Eden區的From塊中存活的物件, 兩者的體積超過了Eden區中To塊的大小, Young GC的悲觀策略將使這些存活的物件都遷移到Old區, 而此時Old區的大小不足以容納這些物件, 從而發生promotion failed, 程式將暫停很長時間.

  • (Xmx-Xmn) * (1-CMSInitiatingOccupancyFraction / 100) >= (Xmn-Xmn / (SurvivorRatior+2))

    進而推斷出:

    CMSInitiatingOccupancyFraction <= ( (Xmx-Xmn) - (Xmn-Xmn / (SurvivorRatior+2) ) ) / (Xmx-Xmn) * 100

參考資料:

版權宣告

作者: ma_shoufeng(馬瘦風)

您的支援是對博主的極大鼓勵, 感謝您的閱讀.

本文版權歸博主所有, 歡迎轉載, 但未經博主同意必須保留此段宣告, 且在文章頁面明顯位置給出原文連結, 否則博主保留追究法律責任的權利.