1. 程式人生 > >Java GC 分析,JVM生產環境引數例項及分析,JVM詳細配置

Java GC 分析,JVM生產環境引數例項及分析,JVM詳細配置

什麼是 Java GC

Java GC(Garbage Collection,垃圾收集,垃圾回收)機制,是Java與C++/C的主要區別之一,作為Java開發者,一般不需要專門編寫記憶體回收和垃圾清理程式碼,對記憶體洩露和溢位的問題,也不需要像C程式設計師那樣戰戰兢兢。這是因為在Java虛擬機器中,存在自動記憶體管理和垃圾清掃機制。概括地說,該機制對JVM(Java Virtual Machine)中的記憶體進行標記,並確定哪些記憶體需要回收,根據一定的回收策略,自動的回收記憶體,永不停息(Nerver Stop)的保證JVM中的記憶體空間,防止出現記憶體洩露和溢位問題。

在Java語言出現之前,就有GC機制的存在,如Lisp語言),Java GC機制已經日臻完善,幾乎可以自動的為我們做絕大多數的事情。然而,如果我們從事較大型的應用軟體開發,曾經出現過記憶體優化的需求,就必定要研究Java GC機制。

簡單總結一下,Java GC就是通過GC收集器回收不在存活的物件,保證JVM更加高效的運轉。

如何獲取 Java GC日誌

一般情況可以通過兩種方式來獲取GC日誌,一種是使用命令動態檢視,一種是在容器中設定相關引數列印GC日誌。

命令動態檢視

Java 自動的工具行命令,jstat可以用來動態監控JVM記憶體的使用,統計垃圾回收的各項資訊。

比如常用命令, jstat-gc 統計垃圾回收堆的行為:

$ jstat -gc 1262

也可以設定間隔固定時間來列印:

$ jstat -gc 1262 2000 20

這個命令意思就是每隔2000ms輸出1262的gc情況,一共輸出20次。

GC引數

JVM的GC日誌的主要引數包括如下幾個:

  • -XX:+PrintGC 輸出GC日誌
  • -XX:+PrintGCDetails 輸出GC的詳細日誌
  • -XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
  • -XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2017-09-04T21:53:59.234+0800)
  • -XX:+PrintHeapAtGC 在進行GC的前後打印出堆的資訊
  • -Xloggc:../logs/gc.log 日誌檔案的輸出路徑

在生產環境中,根據需要配置相應的引數來監控JVM執行情況。

Tomcat 設定示例

我們經常在tomcat的啟動引數中新增JVM相關引數,這裡有一個典型的示例:

JAVA_OPTS="-server -Xms2000m -Xmx2000m -Xmn800m -XX:PermSize=64m -XX:MaxPermSize=256m -XX:SurvivorRatio=4

-verbose:gc -Xloggc:$CATALINA_HOME/logs/gc.log 

-Djava.awt.headless=true 

-XX:+PrintGCTimeStamps -XX:+PrintGCDetails 

-Dsun.rmi.dgc.server.gcInterval=600000 -Dsun.rmi.dgc.client.gcInterval=600000

-XX:+UseConcMarkSweepGC -XX:MaxTenuringThreshold=15"

根據上面的引數我們來做一下解析:

-Xms2000m-Xmx2000m-Xmn800m-XX:PermSize=64m-XX:MaxPermSize=256m
Xms,即為jvm啟動時得JVM初始堆大小,Xmx為jvm的最大堆大小,xmn為新生代的大小,permsize為永久代的初始大小,MaxPermSize為永久代的最大空間。

-XX:SurvivorRatio=4
SurvivorRatio為新生代空間中的Eden區和救助空間Survivor區的大小比值,預設是32,也就是說Eden區是 Survivor區的32倍大小,要注意Survivo是有兩個區的,因此Surivivor其實佔整個young genertation的1/34。調小這個引數將增大survivor區,讓物件儘量在survitor區呆長一點,減少進入年老代的物件。去掉救助空間的想法是讓大部分不能馬上回收的資料儘快進入年老代,加快年老代的回收頻率,減少年老代暴漲的可能性,這個是通過將-XX:SurvivorRatio 設定成比較大的值(比如65536)來做到。

-verbose:gc-Xloggc:$CATALINA_HOME/logs/gc.log
將虛擬機器每次垃圾回收的資訊寫到日誌檔案中,檔名由file指定,檔案格式是平檔案,內容和-verbose:gc輸出內容相同。

-Djava.awt.headless=true Headless模式是系統的一種配置模式。在該模式下,系統缺少了顯示裝置、鍵盤或滑鼠。

-XX:+PrintGCTimeStamps-XX:+PrintGCDetails
設定gc日誌的格式

-Dsun.rmi.dgc.server.gcInterval=600000-Dsun.rmi.dgc.client.gcInterval=600000
指定rmi呼叫時gc的時間間隔

-XX:+UseConcMarkSweepGC-XX:MaxTenuringThreshold=15 採用併發gc方式,經過15次minor gc 後進入年老代。

GC優化是必要的嗎?

或者更準確地說,GC優化對Java基礎服務來說是必要的嗎?答案是否定的,事實上GC優化對Java基礎服務來說在有些場合是可以省去的,但前提是這些正在執行的Java系統,必須包含以下引數或行為:

  • 記憶體大小已經通過-Xms和-Xmx引數指定過
  • 執行在server模式下(使用-server引數)
  • 系統中沒有殘留超時日誌之類的錯誤日誌

換句話說,如果你在執行時沒有手動設定記憶體大小並且打印出了過多的超時日誌,那你就需要對系統進行GC優化。

不過你需要時刻謹記一句話:GC tuning is the last task to be done.

現在來想一想GC優化的最根本原因,垃圾收集器的工作就是清除Java建立的物件,垃圾收集器需要清理的物件數量以及要執行的GC數量均取決於已建立的物件數量。因此,為了使你的系統在GC上表現良好,首先需要減少建立物件的數量。

俗話說“冰凍三尺非一日之寒”,我們在編碼時要首先要把下面這些小細節做好,否則一些瑣碎的不良程式碼累積起來將讓GC的工作變得繁重而難於管理:

  • 使用 StringBuilder或 StringBuffer來代替 String
  • 儘量少輸出日誌

儘管如此,仍然會有我們束手無策的情況。XML和JSON解析過程往往佔用了最多的記憶體,即使我們已經儘可能地少用String、少輸出日誌,仍然會有大量的臨時記憶體(大約10-100MB)被用來解析XML或JSON檔案,但我們又很難棄用XML和JSON。在此,你只需要知道這一過程會佔據大量記憶體即可。

如果在經過幾次重複的優化後應用程式的記憶體用量情況有所改善,那麼久可以啟動GC優化了。

總結了GC優化的兩個目的:

  • 將進入老年代的物件數量降到最低
  • 減少Full GC的執行時間

將進入老年代的物件數量降到最低
除了可以在JDK 7及更高版本中使用的G1收集器以外,其他分代GC都是由Oracle JVM提供的。關於分代GC,就是物件在Eden區被建立,隨後被轉移到Survivor區,在此之後剩餘的物件會被轉入老年代。也有一些物件由於佔用記憶體過大,在Eden區被建立後會直接被傳入老年代。老年代GC相對來說會比新生代GC更耗時,因此,減少進入老年代的物件數量可以顯著降低Full GC的頻率。你可能會以為減少進入老年代的物件數量意味著把它們留在新生代,事實正好相反,新生代記憶體的大小是可以調節的。

降低Full GC的時間
Full GC的執行時間比Minor GC要長很多,因此,如果在Full GC上花費過多的時間(超過1s),將可能出現超時錯誤。

  • 如果通過減小老年代記憶體來減少Full GC時間,可能會引起 OutOfMemoryError或者導致Full GC的頻率升高。
  • 另外,如果通過增加老年代記憶體來降低Full GC的頻率,Full GC的時間可能因此增加。

因此,你需要把老年代的大小設定成一個“合適”的值。

影響GC效能的引數

正如在系列的第一篇文章《理解Java GC》末尾提到的,不要幻想著“如果有人用他設定的GC引數獲取了不錯的效能,我們為什麼不復制他的引數設定呢?”,因為對於不用的Web服務,它們建立的物件大小和生命週期都不相同。

舉一個簡單的例子,如果一個任務的執行條件是A,B,C,D和E,另一個完全相同的任務執行條件只有A和B,那麼哪一個任務執行速度更快呢?作為常識來講,答案很明顯是後者。

Java GC引數的設定也是這個道理,設定好幾個引數並不會提升GC執行的速度,反而會使它變得更慢。GC優化的基本原則是將不同的GC引數應用到兩個及以上的伺服器上然後比較它們的效能,然後將那些被證明可以提高效能或減少GC執行時間的引數應用於最終的工作伺服器上。

下面這張表展示了與記憶體大小相關且會影響GC效能的GC引數

型別 引數 描述
堆記憶體大小 -Xms 啟動JVM時堆記憶體的大小
-Xmx 堆記憶體最大限制
新生代空間大小 -XX:NewRatio 新生代和老年代的記憶體比
-XX:NewSize 新生代記憶體大小
-XX:SurvivorRatio Eden區和Survivor區的記憶體比

在進行GC優化時最常用的引數是 -Xms, -Xmx和 -XX:NewRatio。 -Xms和 -Xmx引數通常是必須的,所以 NewRatio的值將對GC效能產生重要的影響。

有些人可能會問如何設定永久代記憶體大小,你可以用 -XX:PermSize和 -XX:MaxPermSize引數來進行設定,但是要記住,只有當出現 OutOfMemoryError錯誤時你才需要去設定永久代記憶體。

還有一個會影響GC效能的因素是垃圾收集器的型別,下表展示了關於GC型別的可選引數(基於JDK 6.0):

GC型別 引數 備註
Serial GC -XX:+UseSerialGC
Parallel GC -XX:+UseParallelGC
-XX:ParallelGCThreads=value
Parallel Compacting GC -XX:+UseParallelOldGC
CMS GC -XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=value
-XX:+UseCMSInitiatingOccupancyOnly
G1 -XX:+UnlockExperimentalVMOptions
-XX:+UseG1GC
在JDK 6中這兩個引數必須配合使用

除了G1收集器外,可以通過設定上表中每種型別第一行的引數來切換GC型別,最常見的非侵入式GC就是Serial GC,它針對客戶端系統進行了特別的優化。

會影響GC效能的引數還有很多,但是上述的引數會帶來最顯著的效果,請切記,設定太多的引數並不一定會提升GC的效能。

網上某個牛人的配置 :每天幾百萬pv一點問題都沒有,網站沒有停頓:

$JAVA_ARGS
.=
"
-Dresin.home=$SERVER_ROOT
-server
-Xms6000M
-Xmx6000M
-Xmn500M
-XX:PermSize=500M
-XX:MaxPermSize=500M
-XX:SurvivorRatio=65536
-XX:MaxTenuringThreshold=0
-Xnoclassgc
-XX:+DisableExplicitGC
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSClassUnloadingEnabled
-XX:-CMSParallelRemarkEnabled
-XX:CMSInitiatingOccupancyFraction=90
-XX:SoftRefLRUPolicyMSPerMB=0
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log
";

說明一下, -XX:SurvivorRatio=65536 -XX:MaxTenuringThreshold=0就是去掉了救助空間; 

-Xnoclassgc禁用類垃圾回收,效能會高一點; 

-XX:+DisableExplicitGC禁止System.gc(),免得程式設計師誤呼叫gc方法影響效能; 

-XX:+UseParNewGC,對年輕代採用多執行緒並行回收,這樣收得快; 

帶CMS引數的都是和併發回收相關的,不明白的可以上網搜尋; 

CMSInitiatingOccupancyFraction,這個引數設定有很大技巧,基本上滿足(Xmx-Xmn)*(100-CMSInitiatingOccupancyFraction)/100>=Xmn就不會出現promotion failed。在我的應用中Xmx是6000,Xmn是500,那麼Xmx-Xmn是5500兆,也就是年老代有5500兆,CMSInitiatingOccupancyFraction=90說明年老代到90%滿的時候開始執行對年老代的併發垃圾回收(CMS),這時還剩10%的空間是5500*10%=550兆,所以即使Xmn(也就是年輕代共500兆)裡所有物件都搬到年老代裡,550兆的空間也足夠了,所以只要滿足上面的公式,就不會出現垃圾回收時的promotion failed; 

SoftRefLRUPolicyMSPerMB這個引數我認為可能有點用,官方解釋是softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap,我覺得沒必要等1秒; 

-Xmx4000M
-Xms4000M
-Xmn600M
-XX:PermSize=500M
-XX:MaxPermSize=500M
-Xss256K
-XX:+DisableExplicitGC
-XX:SurvivorRatio=1
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSClassUnloadingEnabled
-XX:LargePageSizeInBytes=128M
-XX:+UseFastAccessorMethods
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=80
-XX:SoftRefLRUPolicyMSPerMB=0
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log

改進方案:

上面方法不太好,因為沒有用到救助空間,所以年老代容易滿,CMS執行會比較頻繁。我改善了一下,還是用救助空間,但是把救助空間加大,這樣也不會有promotion failed。

具體操作上,32位Linux和64位Linux好像不一樣,64位系統似乎只要配置MaxTenuringThreshold引數,CMS還是有暫停。為了解決暫停問題和promotion failed問題,最後我設定-XX:SurvivorRatio=1 ,並把MaxTenuringThreshold去掉,這樣即沒有暫停又不會有promotoin failed,而且更重要的是,年老代和永久代上升非常慢(因為好多物件到不了年老代就被回收了),所以CMS執行頻率非常低,好幾個小時才執行一次,這樣,伺服器都不用重啟了。