天天看點

關于JVM的垃圾回收(GC) 這可能是你想了解的

本篇文章從Java對象的分類、Java對象生成時的記憶體申請過程出發, 進而對JVM中GC的類型(CMS、G1等)、GC的觸發條件作了講解, 最後詳細介紹關于GC的配置參數, 提供多種關于優化GC政策的實踐經驗.

目錄

  • 1 JVM中Java對象的分類
  • 2 JVM的GC類型及觸發條件
    • 2.1 Young GC
    • 2.2 Full GC
  • 3 Java對象生成時的記憶體申請過程
  • 3 Oracle JDK中的垃圾收集器
    • 3.1 串行收集器(Serial Collector)
    • 3.2 并行收集器(Parallel Collector)
    • 3.3 并發收集器(Concurrent Collector)
    • 3.4 G1收集器(Garbage First GC)
    • 3.5 其他概念說明
  • 4 GC的配置參數
    • 4.1 參數名稱的說明
      • 4.1.1 标準參數(-)
      • 4.1.2 非标準參數(-X)
      • 4.1.3 非穩定參數(-XX)
    • 4.2 串行GC參數
    • 4.3 并行GC參數(吞吐量優先)
    • 4.4 并發GC參數(響應時間優先)
    • 4.5 G1 GC參數
    • 4.6 通用GC參數
    • 4.7 其他說明
  • 參考資料
  • 版權聲明

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

JVM根據運作于其中的對象的生存時間, 将它們分為3種, 并分别存放在JVM的不同記憶體區域中. 這種對象存放空間的管理方式叫做Generation管理方式.

(1) Young Generation (新生代, 又稱年輕代): 用于存放"早逝"對象(即瞬時對象), 一般的Java應用中, 80%的對象都是"朝生息滅"的, 比如在建立對象或調用方法時使用的臨時對象或局部變量.

(2) Tenured Generation (老年代): 用于存放"駐留"對象(即被引用較長時間的對象). 往往展現為一個大型程式中的全局對象或長時間被使用的對象.

(3) Perm Generation (永久代): 用于存放"永久"對象. 這些對象管理着運作于JVM中的類和方法.

又叫Minor GC(次收集), Young GC經常發生, 且其每次消耗的時間較短.

—— 它隻對Young Generation中的對象進行垃圾收集.

觸發條件:

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

—— 程式運作過程中, 始終有一個 Survivor 區是完全處于空閑狀态的, 如果不是, 說明應用程式出現故障了.

又叫Major GC(主收集), 是對整個Java Heap中的對象(不包括永久代/元空間)進行垃圾收集.

Full 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中各個區域空間的動态變化.

(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

調大(一般256MB能滿足絕大多數應用程式需求).

隻有一條GC線程, 運作時需要暫停使用者程式(Stop-The-World).

實作: Serial(用于新生代, 采用複制算法)、serial old(用于老年代, 采用标記-整理算法).

有多條GC線程, 運作時也需要暫停使用者程式(Stop-The-World).

實作: ParNew(用于新生代, 采用複制算法)、Parallel Scavenge(用于新生代, 采用複制算法)、Parallel Old(用于老年代, 采用标記-整理算法).

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

實作: Concurrent Mark Sweep(CMS, 用于老年代, 采用标記-清除算法).

并發(concurrent)與并行(parallel)的比較:

(1) 并發就是兩個任務(A和B)需要獨立運作, 在任務A結束之前, 任務B開始執行 —— 即表面上多個任務同時執行.

(2) 并行, 類比串行, 是微觀概念, 即在每一個時刻都有多個任務在同時執行, 形象點了解為多管齊下, 串行可了解為單列隊列, 同一時刻隻能執行一個任務.

(3) 事實上, 并行是并發的一種實作方式, 還有一種并發的實作方式, 即我們熟悉的時間片切換 —— 任務A執行一段時間, CPU再切換到任務B執行一段時間, 如此交替執行. 時間片切換在微觀上仍然是串行 —— 某一具體時刻隻有一個任務在執行, 而在宏觀上, 即一段時間内, 有多個任務得到了執行.

(4) 總結: 并行必須在多核多處理器或分布式系統(本質還是多核多處理器)中才能發生, 而單核處理器上隻能發生時間片切換.

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

(1) 與CMS收集器相比, G1收集器的優勢:

① 基于标記-整理算法, 不會産生大量的記憶體碎片;

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

(2) G1收集器的實作原理:

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

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

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

(1) 為了更大程度地榨取機器性能, 新生代的收集器都使用了複制算法, 老年代的收集器都使用 标記-清除 或 标記-整理 算法. 關于各算法詳情, 請參閱: JVM記憶體管理———垃圾搜集器參數精解.

(2) 在實際應用中, 需要對JVM的新生代、老年代分别選擇合适的垃圾收集器.

(3) 這裡新生代和老年代都分别有三種實作, 但由于收集器的實作方式不同, 部分組合無法一起配合工作, 經過驗證, 這六種垃圾收集器隻有六種可用組合.

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

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

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

各 JVM 廠商應該全部實作這些參數的功能, 但并不能保證這些廠商提供的 JVM 中都實作了這些功能, 且不保證向後相容;

此類參數在各個 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. 
           

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

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

-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區的比例, 
# 以達到目标系統規定的最低響應時間或收集頻率等. 建議在使用并行收集器時始終開啟此選項. 
           

-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已不建議使用). 
           

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

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

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

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

-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, 客戶機中不使用就立即清除. 
           

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

(2) 關于 CMS 收集器的 Promotion Failed 和 Concurrent Mode Failure :

  • Promotion Failed

    , 晉升失敗: 發生Young GC時, Eden區存活的對象 和 Eden區的From塊中存活的對象, 兩者的體積超過了Eden區中To塊的大小, Young GC的悲觀政策将使這些存活的對象都遷移到老年代, 而此時老年代的大小不足以容納這些對象, 進而發生promotion failed , 程式将暫停很長時間.
  • Concurrent Mode Failure

    , 并發修改失敗: 在執行GC的過程中, 恰好有對象要晉升至老年代, 而此時老年代的空間不足, 進而造成

    Concurrent Mode Failure

    .
  • 這兩種情況都可能觸發Full GC. 要注意不要一次性占用太多的記憶體, 或對JVM的配置進行一定的優化.
  • CMSInitiatingOccupancyFraction

    的設定技巧: 參考: CMSInitiatingOccupancyFraction計算公式

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

    進而推斷出:

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

JVM系列三:JVM參數設定、分析

G1(Garbage-First)垃圾回收器

JVM記憶體管理——垃圾搜集器參數精解

作者: 馬瘦風

出處: 部落格園 馬瘦風的部落格

您的支援是對部落客的極大鼓勵, 感謝您的閱讀.

本文版權歸部落客所有, 歡迎轉載, 但請保留此段聲明, 并在文章頁面明顯位置給出原文連結, 否則部落客保留追究相關人員法律責任的權利.