JVM原理講解和調優

擊水三千里發表於2019-01-10

一、什麼是JVM

    JVM是Java Virtual Machine(Java虛擬機器)的縮寫,JVM是一種用於計算裝置的規範,它是一個虛構出來的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。

    Java語言的一個非常重要的特點就是與平臺的無關性。而使用Java虛擬機器是實現這一特點的關鍵。一般的高階語言如果要在不同的平臺上執行,至少需要編譯成不同的目的碼。而引入Java語言虛擬機器後,Java語言在不同平臺上執行時不需要重新編譯。Java語言使用Java虛擬機器遮蔽了與具體平臺相關的資訊,使得Java語言編譯程式只需生成在Java虛擬機器上執行的目的碼(位元組碼),就可以在多種平臺上不加修改地執行。Java虛擬機器在執行位元組碼時,把位元組碼解釋成具體平臺上的機器指令執行。這就是Java的能夠“一次編譯,到處執行”的原因。

   從Java平臺的邏輯結構上來看,我們可以從下圖來了解JVM:

標題

    從上圖能清晰看到Java平臺包含的各個邏輯模組,也能瞭解到JDK與JRE的區別,對於JVM自身的物理結構,我們可以從下圖鳥瞰一下:

 

二、JAVA程式碼編譯和執行過程

Java程式碼編譯是由Java原始碼編譯器來完成,流程圖如下所示:

Java位元組碼的執行是由JVM執行引擎來完成,流程圖如下所示:

Java程式碼編譯和執行的整個過程包含了以下三個重要的機制:

Java原始碼編譯機制

類載入機制

類執行機制

Java原始碼編譯機制

Java 原始碼編譯由以下三個過程組成:

分析和輸入到符號表

註解處理

語義分析和生成class檔案

流程圖如下所示:

 

最後生成的class檔案由以下部分組成:

結構資訊。包括class檔案格式版本號及各部分的數量與大小的資訊

後設資料。對應於Java原始碼中宣告與常量的資訊。包含類/繼承的超類/實現的介面的宣告資訊、域與方法宣告資訊和常量池

方法資訊。對應Java原始碼中語句和表示式對應的資訊。包含位元組碼、異常處理器表、求值棧與區域性變數區大小、求值棧的型別記錄、除錯符號資訊

類載入機制

JVM的類載入是通過ClassLoader及其子類來完成的,類的層次關係和載入順序可以由下圖來描述:

載入過程中會先檢查類是否被已載入,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已載入就視為已載入此類,保證此類只所有ClassLoader載入一次。而載入的順序是自頂向下,也就是說當發現這個類沒有的時候會先去讓自己的父類去載入,父類沒有再讓兒子去載入,那麼在這個例子中我們自己寫的String應該是被Bootstrap ClassLoader載入了,所以App ClassLoader就不會再去載入我們寫的String類了,導致我們寫的String類是沒有被載入的。 

1)Bootstrap ClassLoader

負責載入$JAVA_HOME中jre/lib/rt.jar裡所有的class,由C++實現,不是ClassLoader子類

2)Extension ClassLoader

負責載入java平臺中擴充套件功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包

3)App ClassLoader

負責記載classpath中指定的jar包及目錄中class

4)Custom ClassLoader

    屬於應用程式根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader載入過程中會先檢查類是否被已載入,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已載入就視為已載入此類,保證此類只所有ClassLoader載入一次。而載入的順序是自頂向下,也就是由上層來逐層嘗試載入此類。

 

類執行機制

JVM是基於棧的體系結構來執行class位元組碼的。執行緒建立後,都會產生程式計數器(PC)和棧(Stack),程式計數器存放下一條要執行的指令在方法內的偏移量,棧中存放一個個棧幀,每個棧幀對應著每個方法的每次呼叫,而棧幀又是有區域性變數區和運算元棧兩部分組成,區域性變數區用於存放方法中的區域性變數和引數,運算元棧中用於存放方法執行過程中產生的中間結果。棧的結構如下圖所示:

 

三、JVM記憶體管理和垃圾回收

3.1JVM記憶體組成結構

JVM棧由堆、棧、本地方法棧、方法區等部分組成,結構圖如下所示:

1)堆

    所有通過new建立的物件的記憶體都在堆中分配,堆的大小可以通過-Xmx和-Xms來控制。堆被劃分為新生代和舊生代,新生代又被進一步劃分為Eden和Survivor區,最後Survivor由From Space和To Space組成,結構圖如下所示:

新生代。新建的物件都是用新生代分配記憶體,Eden空間不足的時候,會把存活的物件轉移到Survivor中,新生代大小可以由-Xmn來控制,也可以用-XX:SurvivorRatio來控制Eden和Survivor的比例

老年代。用於存放新生代中經過多次垃圾回收仍然存活的物件

持久帶(Permanent Space)實現方法區,主要存放所有已載入的類資訊,方法資訊,常量池等等。可通過-XX:PermSize和-XX:MaxPermSize來指定持久帶初始化值和最大值。Permanent Space並不等同於方法區,只不過是Hotspot JVM用Permanent Space來實現方法區而已,有些虛擬機器沒有Permanent Space而用其他機制來實現方法區。

-Xmx:最大堆記憶體,如:-Xmx512m

-Xms:初始時堆記憶體,如:-Xms256m

-XX:MaxNewSize:最大年輕區記憶體

-XX:NewSize:初始時年輕區記憶體.通常為 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間為 = Eden + 1 個 Survivor,即 90%

-XX:MaxPermSize:最大持久帶記憶體

-XX:PermSize:初始時持久帶記憶體

-XX:+PrintGCDetails。列印 GC 資訊

 -XX:NewRatio 新生代與老年代的比例,如 –XX:NewRatio=2,則新生代佔整個堆空間的1/3,老年代佔2/3

 -XX:SurvivorRatio 新生代中 Eden 與 Survivor 的比值。預設值為 8。即 Eden 佔新生代空間的 8/10,另外兩個 Survivor 各佔 1/10

 

2)棧

    每個執行緒執行每個方法的時候都會在棧中申請一個棧幀,每個棧幀包括區域性變數區和運算元棧,用於存放此次方法呼叫過程中的臨時變數、引數和中間結果。

   -xss:設定每個執行緒的堆疊大小. JDK1.5+ 每個執行緒堆疊大小為 1M,一般來說如果棧不是很深的話, 1M 是絕對夠用了的。

java棧的結構

3)本地方法棧

用於支援native方法的執行,儲存了每個native方法呼叫的狀態

4)方法區

存放了要載入的類資訊、靜態變數、final型別的常量、屬性和方法資訊。JVM用持久代(Permanet Generation)來存放方法區,可通過-XX:PermSize和-XX:MaxPermSize來指定最小值和最大值

5)程式計數器

是一塊較小的記憶體空間,可以看作是當前執行緒所執行的位元組碼的行號指示器。程式中的分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器完成。由於多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,故該區域為執行緒私有的記憶體。

 

3.2垃圾回收策略分類

如何判斷物件“已死”?

思想:從根節點做可達性分析,判斷物件是否可以被回收

可以作為根節點的物件:類載入器、Thread、虛擬機器棧的本地變數表、static 成員、常量引用、本地方法棧的變數

物件分配:物件優先在 Eden 區分配、大物件直接進入Old 區(-XX:PretenureSizeThreshold)、長期存活物件進入 Old 區(-XX:MaxTenuringThreshold, -XX:+PrintTenuringDistribution, -XX:TargetSurvivorRatio

GC最基礎的演算法有三種:標記 -清除演算法、複製演算法、標記-整理演算法,我們常用的垃圾回收器一般都採用分代收集演算法。

 

標記-清除(Mark-Sweep):

    此演算法執行分兩階段。第一階段從引用根節點開始標記所有被引用的物件,第二階段遍歷整個堆,把未標記的物件清除。此演算法需要暫停整個應用,同時,會產生記憶體碎片。

 

複製(Copying):

    此演算法把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾回收時,遍歷當前使用區域,把正在使用中的物件複製到另外一個區域中。演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理,不會出現“碎片”問題。當然,此演算法的缺點也是很明顯的,就是需要兩倍記憶體空間。

 

標記-整理(Mark-Compact):

    此演算法結合了“標記-清除”和“複製”兩個演算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用物件,第二階段遍歷整個堆,清除未標記物件並且把存活物件“壓縮”到堆的其中一塊,按順序排放。此演算法避免了“標記-清除”的碎片問題,同時也避免了“複製”演算法的空間問題。

備註:灰色代表未被標記的物件;白色代表空閒區域;綠色,藍色代表被標記的物件

 

3.3.GC的種類

1.Minor GC:
      Minor GC指新生代GC,即發生在新生代(包括Eden區和Survivor區)的垃圾回收操作,當新生代無法為新生物件分配記憶體空間的時候,會觸發Minor GC。因為新生代中大多數物件的生命週期都很短,所以發生Minor GC的頻率很高,雖然它會觸發stop-the-world,但是它的回收速度很快。
2.Major GC
       Major GC清理Tenured區,用於回收老年代,出現Major GC通常會出現至少一次Minor GC。
3.Full GC
      Full GC是針對整個新生代、老生代、元空間(metaspace,java8以上版本取代perm gen)的全域性範圍的GC。Full GC不等於Major GC,也不等於Minor GC+Major GC,發生Full GC需要看使用了什麼垃圾收集器組合,才能解釋是什麼樣的垃圾回收。

4.MixedGC

     回收所有的 Young和部分的 Old,只有G1有這個模式

 

3.4JVM分別對新生代和老年代採用不同的垃圾回收機制

3.4.1新生代的GC

新生代通常存活時間較短,因此基於複製演算法來進行回收,所謂複製演算法就是掃描出存活的物件,並複製到一塊新的完全未使用的空間中.

對應於新生代:就是在Eden和其中一個Survivor,複製到另一個之間Survivor空間中,然後清理掉原來就是在Eden和其中一個Survivor中的物件

在執行機制上JVM提供了序列GC(Serial GC)、並行回收GC(Parallel Scavenge)和併發GC(ParNew)

1)序列GC

    在整個掃描和複製過程採用單執行緒的方式來進行,主要用在嵌入式等記憶體比較小的程式,是client級別預設的GC方式,可以通過-XX:+UseSerialGC來強制指定

2)並行回收GC

    多條垃圾收集起並行工作,但是使用者執行緒時阻塞的,適用於互動性比較弱的場景,是server級別預設採用的GC方式,可用-XX:+UseParallelGC來強制指定,用-XX:ParallelGCThreads=4來指定執行緒數。Parallel Scavenge(-XX:+UseParallelGC)框架下,預設是在要觸發full GC前先執行一次young GC,並且兩次GC之間能讓應用程式稍微執行一小下,以期降低full GC的暫停時間(因為young GC會盡量清理了young gen的死物件,減少了full GC的工作量)

3)併發GC 

使用者執行緒和垃圾回收器併發執行,適用於響應時間優先,互動性比較強的場景,併發GC的觸發條件就不太一樣。以CMS GC (-XX:+UseConcMarkSweepGC -XX:+UseParNewGC)為例,它主要是定時去檢查old gen的使用量,當使用量超過了觸發比例就會啟動一次CMS GC,對old gen做併發收集

 

3.4.2老年代的GC

   老年代與新生代不同,物件存活的時間比較長,比較穩定,因此採用標記(Mark)演算法來進行回收,所謂標記就是掃描出存活的物件,然後再進行回收未被標記的物件,回收後對用空出的空間要麼進行合併,要麼標記出來便於下次進行分配,總之就是要減少記憶體碎片帶來的效率損耗。在執行機制上JVM提供了序列GC(Serial Old)、並行 回收 GC(parallel Old)和併發GC。[CMS (-XX:+UseConcMarkSweepGC -XX:+UseParNewGC), G1(-XX:UseG1GC)],具體演算法細節還有待進一步深入研究。

 

3.4.2.1 CMS 收集器

優點:併發收集、低停頓

缺點:

1)CMS收集器對CPU資源非常敏感。在併發階段,它雖然不會導致使用者執行緒停頓,但是會因為佔用了一部分執行緒而導致應用程式變慢,總吞吐量會降低。

2)CMS收集器無法處理浮動垃圾,可能會出現“Concurrent Mode Failure(併發模式故障)”失敗而導致Full GC產生。

3)CMS是一款“標記--清除”演算法實現的收集器,容易出現大量空間碎片。當空間碎片過多,將會給大物件分配帶來很大的麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前物件,不得不提前觸發一次Full GC。
 

CMS過程:

初始標記 :(STW )在這個階段,需要虛擬機器停頓正在執行的任務,官方的叫法STW(Stop The Word)。這個過程從垃圾回收的"根物件"開始,只掃描到能夠和"根物件"直接關聯的物件,並作標記。所以這個過程雖然暫停了整個JVM,但是很快就完成了。

併發標記 :這個階段緊隨初始標記階段,在初始標記的基礎上繼續向下追溯標記。併發標記階段,應用程式的執行緒和併發標記的執行緒併發執行,所以使用者不會感受到停頓。

併發預清理 :併發預清理階段仍然是併發的。在這個階段,虛擬機器查詢在執行併發標記階段新進入老年代的物件(可能會有一些物件從新生代晉升到老年代, 或者有一些物件被分配到老年代)。通過重新掃描,減少下一個階段"重新標記"的工作,因為下一個階段會Stop The World。

重新標記 :(STW )這個階段會暫停虛擬機器,收集器執行緒掃描在CMS堆中剩餘的物件。掃描從"跟物件"開始向下追溯,並處理物件關聯。

併發清理 :清理垃圾物件,這個階段收集器執行緒和應用程式執行緒併發執行。

併發重置 :這個階段,重置CMS收集器的資料結構,等待下一次垃圾回收。

 

相關引數:

-XX:ConcGCThreads:併發的 GC 執行緒數

-XX:+UseCMSCompactAtFullCollection:FullGC 之後做壓縮

-XX:CMSFullGCsBeforeCompaction:多少次 FullGC之後壓縮一次

-XX:CMSInitiatingOccupancyFraction:觸發 FullGC

-XX:+UseCMSInitiatingOccupancyOnly:是否動態可調

-XX:+CMSScavengeBeforeRemark:FullGC之前先做 YGC

-XX:+CMSClassUnloadingEnable:啟用回收Perm 區
 

3.4.2.2、G1收集器

G1 jdk8開始,推薦使用(重點學習,jdk9中已經設為預設垃圾收集器)

 

G1(global concurrent marking)的執行過程分為五個步驟:

初始標記(initial mark,STW) 
在此階段,G1 GC 對根進行標記。該階段與常規的 (STW) 年輕代垃圾回收密切相關。

根區域掃描(root region scan) 
G1 GC 在初始標記的存活區掃描對老年代的引用,並標記被引用的物件。該階段與應用程式(非 STW)同時執行,並且只有完成該階段後,才能開始下一次 STW 年輕代垃圾回收。

併發標記(Concurrent Marking) 
G1 GC 在整個堆中查詢可訪問的(存活的)物件。該階段與應用程式同時執行,可以被 STW 年輕代垃圾回收中斷

最終標記(Remark,STW) 
該階段是 STW 回收,幫助完成標記週期。G1 GC 清空 SATB 緩衝區,跟蹤未被訪問的存活物件,並執行引用處理。把Rember Set log的資料合併到Rember Set中

清除垃圾(Cleanup,STW) 
 

G1具備如下特點:

1、並行於併發:G1能充分利用CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短stop-The-World停頓時間。部分其他收集器原本需要停頓Java執行緒執行的GC動作,G1收集器仍然可以通過併發的方式讓java程式繼續執行。

2、分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間,熬過多次GC的舊物件以獲取更好的收集效果。

3、空間整合:與CMS的“標記--清理”演算法不同,G1從整體來看是基於“標記整理”演算法實現的收集器;從區域性上來看是基於“複製”演算法實現的。


4、可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,
 

G1的幾個概念

Region

傳統的GC收集器將連續的記憶體空間劃分為新生代、老年代和永久代(JDK 8去除了永久代,引入了元空間Metaspace),這種劃分的特點是各代的儲存地址(邏輯地址,下同)是連續的。如下圖所示:

而G1的各代儲存地址是不連續的,每一代都使用了n個不連續的大小相同的Region,每個Region佔有一塊連續的虛擬記憶體地址。如下圖所示:

在上圖中,我們注意到還有一些Region標明瞭H,它代表Humongous,這表示這些Region儲存的是巨大物件(humongous object,H-obj),即大小大於等於region一半的物件 

為了減少連續H-objs分配對GC的影響,需要把大物件變為普通的物件,建議增大Region size。 

一個Region的大小可以通過引數-XX:G1HeapRegionSize設定,取值範圍從1M到32M,且是2的指數。如果不設定,那麼G1會根據Heap大小自動決定 

SATB

Snapshot-At-The-Beginning,由字面理解,是GC開始時活著的物件的一個快照。它是通過Root Tracing得到的,作用是維持併發GC的正確性。

那麼它是怎麼維持併發GC的正確性的呢?根據三色標記演算法,我們知道物件存在三種狀態:

  • 白:物件沒有被標記到,標記階段結束後,會被當做垃圾回收掉。
  • 灰:物件被標記了,但是它的field還沒有被標記或標記完。
  • 黑:物件被標記了,且它的所有field也被標記完了。

RSet

記錄了其他 Region中的物件引用本 Region 中物件的關係,全稱是Remembered Set,是輔助GC過程的一種結構,典型的空間換時間工具。邏輯上說每個Region都有一個RSet,RSet記錄了其他Region中的物件引用本Region中物件的關係,屬於points-into結構(誰引用了我的物件),而Card Table則是一種points-out(我引用了誰的物件)的結構

上圖中有三個Region,每個Region被分成了多個Card,在不同Region中的Card會相互引用,Region1中的Card中的物件引用了Region2中的Card中的物件,藍色實線表示的就是points-out的關係,而在Region2的RSet中,記錄了Region1的Card,即紅色虛線表示的關係,這就是points-into。

 

G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是完全Stop The World的

YoungGC

新物件進入 Eden 區

存活物件拷貝到Survivor 區

存活時間達到年齡閾值時,物件晉升到 Old 區

MixedGC

不是 FullGC,選定所有年輕代裡的Region,外加根據global concurrent marking統計得出收益高的若干老年代Region

 

MixedGC時機

-XX:InitiatingHeapOccupancyPercent   堆佔有率到達這個數值時觸發global concurrent marking,預設45%

-XX:G1HeapWastePercent   在global concurrent marking可以知道區有多少空間可以被回收,YGC和Mixed GC之間判斷垃圾佔比是否到達此引數,到達了才會發生Mixed GC

-XX:G1MixedGCLiveThresholdPercent

-XX:G1MixedGCCountTarget

-XX:G1OldGCSetRegionThresholdPercent

 

-XX:+UseG1GC 開啟 G1

-XX:G1HeapRegionSize=n, Region 的大小,1-32M,最多2048個

-XX:MaxGCPauseMillis=200 最大停頓時間

-XX:G1NewSizePercent、-XX:G1MaxNewSizePercent

-XX:G1ReservePercent=10 保留防止 to space溢位

-XX:ParallelGCThreads=n SWT執行緒數

-XX:ConcGCThreads=n 併發執行緒數=1/4*並行

 

需要切換到 G1的情況:

1. 50%以上的堆被存活物件佔用

2. 物件分配和晉升的速度變化非常大

3. 垃圾回收時間特別長,超過了1秒

 

 

3.4.3 GC機制的組合適用

以上各種GC機制是需要組合使用的,指定方式由下表所示:

指定方式

新生代GC方式

舊生代GC方式

-XX:+UseSerialGC

序列GC

序列GC

-XX:+UseParallelGC

並行回收GC

並行GC

-XX:+UseConeMarkSweepGC

並行GC

併發GC

-XX:+UseParNewGC

並行GC

序列GC

-XX:+UseParallelOldGC

並行回收GC

並行GC

-XX:+ UseConeMarkSweepGC

-XX:+UseParNewGC

序列GC

併發GC

不支援的組合

1、-XX:+UseParNewGC -XX:+UseParallelOldGC

2、-XX:+UseParNewGC -XX:+UseSerialGC

標題


3.4.4  GC 日誌的分析

列印日誌相關引數:
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution
例(預設為 ParallelGC, 其它的新增-XX:+UseConcMarkSweepGC或-XX:+UseG1GC即可):
JAVA_OPTS="$JAVA_OPTS -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log"

 

CMS日誌格式(   -XX:+UseConcMarkSweepGC)


 

G1日誌格式YCG

 

G1日誌 global concurrent marking
G1日誌 MixedGC

 

下面的文章是具體實戰下的各種情況調優,雖然裡面的日誌格式不是最新的但是,但是大部分是可以複用的

CMS日誌格式
G1日誌格式
 

視覺化工具分析GC日誌

http://gceasy.io/ 線上工具

GCViewer   mvn clean package -Dmaven.test.skip 生成 jar包,雙擊執行,匯入日誌即可進入圖形化分析頁面

 

四、JVM記憶體調優

4.1理論部分

    首先需要注意的是在對JVM記憶體調優的時候不能只看作業系統級別Java程式所佔用的記憶體,這個數值不能準確的反應堆記憶體的真實佔用情況,因為GC過後這個值是不會變化的,因此記憶體調優的時候要更多地使用JDK提供的記憶體檢視工具,比如JConsole和Java VisualVM。

4.1.1系統級的調優主要的目的

    對JVM記憶體的系統級的調優主要的目的是減少GC的頻率和Full GC的次數,過多的GC和Full GC是會佔用很多的系統資源(主要是CPU),影響系統的吞吐量。特別要關注Full GC,因為它會對整個堆進行整理,導致Full GC一般由於以下幾種情況

1、老年代空間不足
    調優時儘量讓物件在新生代GC時被回收、讓物件在新生代多存活一段時間和不要建立過大的物件及陣列避免直接在舊生代建立物件 

2、Pemanet Generation空間不足
    增大Perm Gen空間,避免太多靜態物件 

    統計得到的GC後晉升到舊生代的平均大小大於舊生代剩餘空間
    控制好新生代和舊生代的比例 

3、System.gc()被顯示呼叫
    垃圾回收不要手動觸發(生產環境一般配置 -XX:+DisableExplicitGC),儘量依靠JVM自身的機制

4.1.2 調優手段

調優手段主要是通過控制堆記憶體的各個部分的比例和GC策略來實現,下面來看看各部分比例不良設定會導致什麼後果

.1)新生代設定過小

    一是新生代GC次數非常頻繁,增大系統消耗;二是導致大物件直接進入舊生代,佔據了舊生代剩餘空間,誘發Full GC

2)新生代設定過大

    一是新生代設定過大會導致老年代過小(堆總量一定),從而誘發Full GC;二是新生代GC耗時大幅度增加

    一般說來新生代佔整個堆1/3比較合適

3)Survivor設定過小

    導致物件從eden直接到達舊生代,降低了在新生代的存活時間

4)Survivor設定過大

    導致eden過小,增加了GC頻率

    另外,通過-XX:MaxTenuringThreshold=n來控制新生代存活時間,儘量讓物件在新生代被回收

 

4.1.3 簡單的GC策略的設定方式

    由記憶體管理和垃圾回收可知新生代和舊生代都有多種GC策略和組合搭配,選擇這些策略對於我們這些開發人員是個難題,JVM提供兩種較為簡單的GC策略的設定方式

1)吞吐量優先

    JVM以吞吐量為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,來達到吞吐量指標。這個值可由-XX:GCTimeRatio=n來設定

2)暫停時間優先

    JVM以暫停時間為指標,自行選擇相應的GC策略及控制新生代與舊生代的大小比例,儘量保證每次GC造成的應用停止時間都在指定的數值範圍內完成。這個值可由-XX:MaxGCPauseRatio=n來設定

 

4.2實戰部分

調優的一般步驟:①首先收集gc日誌,②分析日誌中的關鍵效能指標,③分析GC原因,調優JVM引數。

衡量GC的兩個指標:①吞吐量 ②響應時間。理想情況下是高吞吐量,低響應時間,但現實往往兩個引數是相悖的。

高吞吐量適合場景:科學計算,後臺處理等弱互動場景。

高響應時間適合場景:對響應時間有要求的場景。


 初始引數設定(三種收集器調優可以控制的引數)

PARALLEL_OPTION="-xx:+UseParallelGC -XX:+UseParalleloldGC -XX:MaxGCPauseMills=100 -
XX:GCTimeRatio=99 -XX:YoungGenerationSizeIncrement=30"

CMS_OPTION="-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=92 -
XX:+UseCMSInitiatingOccupancyOnly -XX:+UseCMSCompactAtFullCollection -
XX:CMSFullGCsBeforeCompaction=5"

G1_OPTION="-xx:+UseG1GC -Xms128M -Xmx128 -XX:MetaspaceSize=64M -XX:MaxGCPauseMillis=100 -
XX:+UseStringDeduplication -XX:StringDeduplicationAgeThreshold=3"

 

JAVA_OPTS="$JAVA_OPTS  -XX:+UseG1GC -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=$CATALINA_HOME/logs/ -XX:+PrintGCDetails  -XX:+PrintGCTimeStamps -
XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -Xloggc:$CATALINA_HOME/logs/gc.log 
-XX:+PrintHeapAtGC  -XX:+PrintTenuringDistribution"

4.2.1、Parallel GC調優

指導原則:

  • 除非確定,否則不要設定最大堆記憶體
  • 優先設定吞吐量目標
  • 如果吞吐量目標達不到,調大最大記憶體,不能讓OS使用Swap,如果仍然達不到,降低目標
  • 如果吞吐量能達到,但GC時間太長,則設定停頓時間的目標

設定 Metaspace 大小 -XX:MetaspaceSize=64M -XX:MaxMetaspaceSize=64M

新增吞吐量和停頓時間引數 -XX:GCTimeRatio=99 -XX:MaxGCPauseMillis=100

修改動態擴容增量 -XX:YoungGenerationSizeIncrement=30


 4.2.2、G1 GC調優

指導原則:

  • 年輕代大小:避免使用-Xmn、-XX:NewRatio等顯式設定Young區大小,會覆蓋暫停時間目標
  • 暫停時間目標:暫停時間不要太嚴苛,其吞吐量目標是90%的應用程式時間和10%的垃圾回收時間,太嚴苛會直接影響到吞吐量

同樣的,我們先不設定任何調優引數,只是設定一些初始引數,然後再來做對比,也是以Tomcat為例(之前Parallel GC相關的引數要去掉),如下:

JAVA_OPTS="$JAVA_OPTS -XX:+UseG1GC -XX:+DisableExplicitGC -XX:+HeapDumpOnOutOfMemoryError -
XX:HeapDumpPath=$CATALINA_HOME/logs/ -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -
XX:+PrintGCDateStamps -Xloggc:$CATALINA_HOME/logs/gc.log -XX:+PrintHeapAtGC -
XX:+PrintTenuringDistribution"

和之前一樣,重啟Tomcat完成後,把GC日誌下載到本地,然後上傳到工具上進行分析,可以看到使用了G1後,停頓時間明顯小了很多,但吞吐量變化不大,因為G1是停頓時間優先的收集器: 

從觸發GC的原因可以看到,Metaspace區域發生了一次GC,並且Young GC(Others)的次數也比較多: 

 

同樣的,在頁面頂端,該工具也提示了我們可以調整Metaspace區域的大小: 

那我們就和之前一樣,調大Metaspace區域看看: 

JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=64M ...略..."

 再次將日誌檔案上傳到視覺化工具中進行分析,可以看到吞吐量上去了一些:

而且也沒有再發生Full GC了: 

但是從上圖中可以看到Young GC的次數依然很多,我們可以試著將堆的大小調大一些看看。如下:

JAVA_OPTS="$JAVA_OPTS -Xms128M -Xmx128M ...略..."

再次將日誌檔案上傳到視覺化工具中進行分析,可以看到吞吐量和停頓時間卻變長了一些: 

 

但是Young GC的次數明顯少了很多:

 我們都知道G1是停頓時間優先的收集器,所以我們可以設定一個停頓時間目標,讓G1自己自動調整去達到這個目標。如下:

JAVA_OPTS="$JAVA_OPTS -XX:MaxGCPauseMillis=100 ...略..."

再次將日誌檔案上傳到視覺化工具中進行分析,結果是吞吐量上去了一些,但停頓時間卻變長了一些:

G1收集器的調優引數無非也就這幾個,更多的是要對日誌進行分析以及經驗的積累,才能得出高效的調優方式。 

 

4.3JVM常見配置彙總和參考文章

堆設定

-Xms:初始堆大小

-Xmx:最大堆大小

-XX:NewSize=n:設定年輕代大小

-XX:NewRatio=n:設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代佔整個年輕代年老代和的1/4

-XX:SurvivorRatio=n:年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區佔整個年輕代的1/5

-XX:MaxPermSize=n:設定持久代大小

收集器設定

-XX:+UseSerialGC:設定序列收集器

-XX:+UseParallelGC:設定並行收集器

-XX:+UseParalledlOldGC:設定並行年老代收集器

-XX:+UseConcMarkSweepGC:設定併發收集器

垃圾回收統計資訊

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-Xloggc:filename

並行收集器設定

-XX:ParallelGCThreads=n:設定並行收集器收集時使用的CPU數。並行收集執行緒數。

-XX:MaxGCPauseMillis=n:設定並行收集最大暫停時間

-XX:GCTimeRatio=n:設定垃圾回收時間佔程式執行時間的百分比。公式為1/(1+n)

併發收集器設定

-XX:+CMSIncrementalMode:設定為增量模式。適用於單CPU情況。

-XX:ParallelGCThreads=n:設定併發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集執行緒數。

 

參考文章(要緊跟官方文件):

JVM層GC調優(下)
http://blog.51cto.com/zero01/2150696
jvm的執行時資料區
https://docs.oracle.com/javase/specs/jvms/se8/html/index.html
Metaspace
http://ifeve.com/jvm-troubleshooting-guide-4/
壓縮類空間
https://blog.csdn.net/jijijijwwi111/article/details/51564271
CodeCache
https://blog.csdn.net/yandaonan/article/details/50844806
http://engineering.indeedblog.com/blog/2016/09/job-search-web-app-java-8-migration/
GC調優指南:
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html
如何選擇垃圾收集器
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html
G1最佳實踐
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations
G1 GC的一些關鍵技術
https://zhuanlan.zhihu.com/p/22591838
CMS日誌格式
https://blogs.oracle.com/poonam/understanding-cms-gc-logs
G1日誌格式
https://blogs.oracle.com/poonam/understanding-g1-gc-logs
GC日誌分析工具
http://gceasy.io/   
GCViewer
https://github.com/chewiebug/GCViewer
ZGC:
http://openjdk.java.net/jeps/333

相關文章