1. 程式人生 > >關鍵業務系統的JVM引數推薦(2018仲夏版)

關鍵業務系統的JVM引數推薦(2018仲夏版)

前言1,資料

1. 學習開源專案的啟動指令碼是個不錯的主意,比如ElasticSearch家的Cassandra家的, 附送一篇解釋它的文章

2. VJTools的 jvm-options.sh,伸手黨們最愛,根據自己需要稍微定製一下就行。

3. JVM調優的"標準引數"的各種陷阱 ,R大的文章,在JDK6時寫的,年年期待更新。

 

前言2, -XX:+PrintFlagsFinal列印引數值

當你在網上興沖沖找到一個可優化的引數時,先用-XX: +PrintFlagsFinal看看,它可能已經預設打開了,再找到一個,還是預設打開了...

JDK7與JDK8,甚至JDK7中的不同小版本,有些引數值都不一樣,所以不要輕信網上任何文章,一切以生產環境同版本的JDK打出來的為準。

經常以類似下面的語句去檢視引數,偷懶不起應用,用-version代替。有些引數設定後會影響其他引數,所以也要帶上。

 

java -Xmx1024m -Xms1024m -XX:+UseConcMarkSweepGC -XX:+PrintFlagsFinal -version| grep ParallelGCThreads

對於不同版本里的預設值,建議是順勢而為,JDK在那個版本預設開啟不開啟總有它的理由。安全第一,沒有很好的因由,不要隨便因為網上某篇文章的推薦(包括你現在在讀的這篇)就去設定。

 

1. 效能篇

1.1 建議的效能引數

1. 取消偏向鎖 -XX:-UseBiasedLocking

JDK1.6開始預設開啟的偏向鎖,會嘗試把鎖賦給第一個訪問它的執行緒,取消同步塊上的synchronized原語。如果始終只有一條執行緒在訪問它,就成功略過同步操作以獲得性能提升。

但一旦有第二條執行緒訪問這把鎖,JVM就要撤銷偏向鎖恢復到未鎖定執行緒的狀態,如果開啟安全點日誌,可以看到不少RevokeBiasd的紀錄,像GC一樣Stop The World的幹活,雖然只是很短的停頓,但對於多執行緒併發的應用,取消掉它反而有效能的提升,所以Cassandra就取消了它。
 

2. 加大Integer Cache -XX:AutoBoxCacheMax=20000

Integer i=3;這語句有著 int自動裝箱成Integer的過程,JDK預設只快取 -128 ~ +127的Integer 和 Long,超出範圍的數字就要即時構建新的Integer物件。設為20000後,我們應用的QPS有足足4%的影響。為什麼是2萬呢,因為-XX:+AggressiveOpts裡也是這個值。詳見Java Integer(-128~127)值的==和equals比較產生的思考
 

3. 啟動時訪問並置零記憶體頁面 -XX:+AlwaysPreTouch

啟動時就把引數裡說好了的記憶體全部舔一遍,可能令得啟動時慢上一點,但後面訪問時會更流暢,比如頁面會連續分配,比如不會在晉升新生代到老生代時才去訪問頁面使得GC停頓時間加長。ElasticSearch和Cassandra都打開了它。
 

4. SecureRandom生成加速 -Djava.security.egd=file:/dev/./urandom

此江湖偏方原因為Tomcat的SecureRandom顯式使用SHA1PRNG演算法時,初始因子預設從/dev/random讀取會存在堵塞。額外效果是SecureRandom的預設演算法也變成合適的SHA1了。詳見 SecureRandom的江湖偏方與真實效果
 

1.2 可選的效能引數

1. -XX:+PerfDisableSharedMem

Cassandra家的一個引數,一直沒留意,直到發生高IO時的JVM停頓。原來JVM經常會默默的在/tmp/hperf 目錄寫上一點statistics資料,如果剛好遇到PageCache刷盤,把檔案阻塞了,就不能結束這個Stop the World的安全點了。
禁止JVM寫statistics資料的代價,是jps和jstat 用不了,只能用JMX,而JMX取新老生代的使用百分比還真沒jstat方便,VJTools VJTools裡的vjmxcli彌補了這一點。詳見The Four Month Bug: JVM statistics cause garbage collection pauses

2. -XX:-UseCounterDecay

禁止JIT呼叫計數器衰減。預設情況下,每次GC時會對呼叫計數器進行砍半的操作,導致有些方法一直溫熱,永遠都達不到觸發C2編譯的1萬次的閥值。

3. -XX:-TieredCompilation

多層編譯是JDK8後預設開啟的比較驕傲的功能,先以C1靜態編譯,取樣足夠後C2編譯。

但我們實測,效能最終略降2%,可能是因為有些方法C1編譯後C2不再編譯了。應用啟動時的偶發服務超時也多了,可能是忙於編譯。所以我們將它禁止了,但記得開啟前面的-XX:-UseCounterDecay,避免有些溫熱的方法永遠都要解釋執行。
 

1.3 不建議的效能引數

1. -XX:+AggressiveOpts

一些還沒預設開啟的優化引數集合, -XX:AutoBoxCacheMax是其中的一項。但如前所述,關鍵系統裡不建議開啟。雖然通過-XX:+AggressiveOpts 與 -XX:-AggressiveOpts 的對比,目前才改變了三個引數,但為免以後某個版本的JDK裡默默改變更多激進的配置,還是不要打開了。

2. JIT Compile相關的引數,函式呼叫多少次之後開始編譯的閥值,行內函數大小的閥值等等,不要亂改。

3. -server,在64位多核的linux中,你想設成-client都不行的,所以寫了也是白寫。
 

2. 記憶體與GC篇

2.1 GC策略

為了穩健,還是8G以下的堆還是CMS好了,G1現在雖然是默認了,但其實在小堆裡的表現也沒有比CMS好,還是JDK11的ZGC引人期待。

1.CMS基本寫法

 

-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly

因為我們的監控系統會通過JMX監控記憶體達到90%的狀況,所以設定讓它75%就開始跑了,早點開始也能減少Full GC等意外情況(概念重申,這種主動的CMS GC,和JVM的老生代、永久代、堆外記憶體完全不能分配記憶體了而強制Full GC是不同的概念)。
為了讓這個設定生效,還要設定-XX:+UseCMSInitiatingOccupancyOnly,否則75%只被用來做開始的參考值,後面還是JVM自己算。
 

2. -XX:MaxTenuringThreshold=2

這是改動效果最明顯的一個引數了。物件在Survivor區最多熬過多少次Young GC後晉升到年老代,JDK8裡CMS 預設是6,其他如G1是15。

Young GC是最大的應用停頓來源,而新生代裡GC後存活物件的多少又直接影響停頓的時間,所以如果清楚Young GC的執行頻率和應用裡大部分臨時物件的最長生命週期,可以把它設的更短一點,讓其實不是臨時物件的新生代物件趕緊晉升到年老代,別呆著。

用-XX:+PrintTenuringDistribution觀察下,如果後面幾代的大小總是差不多,證明過了某個年齡後的物件總能晉升到老生代,就可以把晉升閾值設小,比如JMeter裡2就足夠了。
 

3. -XX:+ExplicitGCInvokesConcurrent 但不要-XX:+DisableExplicitGC

​full gc時,使用CMS演算法,不是全程停頓,必選。

但像R大說的,System GC是保護機制(如堆外記憶體滿時清理它的堆內引用物件),禁了system.gc() 未必是好事,只要沒用什麼特別爛的類庫,真有人調了總有調的原因,所以不應該加這個爛大街的引數。
 

4. ParallelRefProcEnabled 和 CMSParallelInitialMarkEnabled

並行的處理Reference物件,如WeakReference,預設為false,除非在GC log裡出現Reference處理時間較長的日誌,否則效果不會很明顯,但我們總是要JVM儘量的並行,所以設了也就設了。同理還有-XX:+CMSParallelInitialMarkEnabled,JDK8已預設開啟,但小版本比較低的JDK7甚至不支援。
 

5. ParGCCardsPerStrideChunk

Linkined的黑科技, 上一個版本的文章不建議開啟,後來發現有些場景的確能減少YGC時間,詳見難道他們說的都是真的,簡單說就是影響YGC時掃描老生代的時間,預設值256太小了,但32K也未必對,需要自己試驗。

-XX:+UnlockDiagnosticVMOptions -XX: ParGCCardsPerStrideChunk=1024
 

2.2 可選的GC引數

1. 併發收集執行緒數

 

ParallelGCThreads=8+( Processor - 8 ) ( 5/8 );
ConcGCThreads = (ParallelGCThreads + 3)/4

比如雙CPU,六核,超執行緒就是24個處理器,小於8個處理器時ParallelGCThreads按處理器數量,大於時按上述公式YGC執行緒數=18, CMS GC執行緒數=5。

CMS GC執行緒數的公式太怪,也有人提議簡單改為YGC執行緒數的1/2。

一些不在乎停頓時間的後臺輔助程式,比如日誌收集的logstash,建議把它減少到2,避免在GC時突然佔用太多CPU核,影響主應用。

而另一些並不獨佔伺服器的應用,比如旁邊跑著一堆sidecar的,也建議減少YGC執行緒數。

一個真實的案例,24核的伺服器,預設18條YGC執行緒,但因為旁邊有個繁忙的Service Mesh Proxy在跑著,這18條執行緒並不能100%的搶到CPU,出現了不合理的慢GC。把執行緒數降低到12條之後,YGC反而快了很多。 所以那些貪心的把YGC執行緒數=CPU 核數的,通常弄巧成拙。
 

2. -XX:-CMSClassUnloadingEnabled

在CMS中清理永久代中的過期的Class而不等到Full GC,JDK7預設關閉而JDK8開啟。看自己情況,比如有沒有執行動態語言指令碼如Groovy產生大量的臨時類。它有時會大大增加CMS的暫停時間。所以如果新類載入並不頻繁,這個引數還是顯式關閉的好。
 

3. -XX:+CMSScavengeBeforeRemark

預設為關閉,在CMS remark前,先執行一次minor GC將新生代清掉,這樣從老生代的物件引用到的新生代物件的個數就少了,停止全世界的CMS remark階段就短一些。如果打開了,會讓一次YGC緊接著一次CMS GC,使得停頓的總時間加長了。

又一個真實案例,CMS GC的時間和當時新生代的大小成比例,新生代很小時很快完成,新生代80%時CMS GC停頓時間超過一秒,這時候就還是打開了划算。

 

2.3 不建議的GC引數

1. -XX:+UseParNewGC

用了CMS,新生代收集預設就是,不用自己設。

 
2. -XX:CMSFullGCsBeforeCompaction
預設為0,即每次full gc都對老生代進行碎片整理壓縮。Full GC 不同於 老生代75%時觸發的CMS GC,只在老生代達到100%,老生代碎片過大無法分配空間給新晉升的大物件,堆外記憶體滿,這些特殊情況裡發生,所以設為每次都進行碎片整理是合適的,詳見此貼裡R大的解釋

 
3.-XX:+GCLockerInvokesConcurrent

我們犯過的錯,不是所有Concurrent字樣的引數都是好引數,加上之後,原本遇上JNI GCLocker只需要補償YGC就夠的,變成要執行YGC + CMS GC了。

 

2.4 記憶體大小的設定

其實JVM除了顯式設定的-Xmx堆記憶體,還有一堆其他佔記憶體的地方(堆外記憶體,執行緒棧,永久代,二進位制程式碼cache),在容量規劃的時候要留意。

關鍵業務系統的伺服器上記憶體一般都是夠的,所以儘管設得寬鬆點。

1. -Xmx, -Xms,

堆記憶體大小,2~4G均可。

 
2. -Xmn or -XX:NewSize or -XX:NewRatio

JDK預設新生代佔堆大小的1/3, 個人喜歡把對半分, 因為增大新生代能減少GC的頻率,如果老生代裡沒多少長期物件的話,佔2/3通常太多了。可以用-Xmn 直接賦值(等於-XX:NewSize and -XX:MaxNewSize同值的縮寫),或把NewRatio設為1來對半分。

 
3. -XX: PermSize=128m -XX:MaxPermSize=512m (JDK7)
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m(JDK8)

現在的應用有Hibernate/Spring這些鬧騰的傢伙AOP之後類都比較多,可以一開始就把初始值從64M設到128M(否則第一次自動擴張會造成大約3秒的JVM停頓),並設一個更大的Max值以求保險。

JDK8的永生代幾乎可用完機器的所有記憶體,同樣設一個128M的初始值,512M的最大值保護一下。

 

2.5 其他記憶體大小的設定
 

1. -Xss

在堆之外,執行緒佔用棧記憶體,預設每條執行緒為1M(以前是256K)。存放方法調用出參入參的棧,區域性變數,標量替換後掉區域性變數等,有人喜歡把它設回256k,節約記憶體並開更多執行緒,有人則會在遇到錯誤後把它再設大點,特別是有很深的JSON解析之類的遞迴呼叫時。
 

2. -XX:SurvivorRatio

新生代中每個存活區的大小,預設為8,即1/10的新生代 1/(SurvivorRatio+2),有人喜歡設小點省點給新生代如Cassandra,但要避免太小使得存活區放不下臨時物件而被迫晉升到老生代,還是從GC日誌裡看實際情況了。
 

3. -XX:MaxDirectMemorySize

堆外記憶體的最大值,預設為Heap區總記憶體減去一個Survivor區的大小,詳見Netty之堆外記憶體掃盲篇,如果肯定用不了這麼多,也可以把它主動設小,來獲得一個比較清晰記憶體佔用預估值,特別是在容器裡。
 

4. -XX:ReservedCodeCacheSize

JIT編譯後二進位制程式碼的存放區,滿了之後就不再編譯,對效能影響很大。初始值為2M, 不開多層編譯時最大值為48M,開了的話JDK7是96M,JDK8是240M。可以在JMX裡看看CodeCache的佔用情況,也可以用VJTools裡的vjtop來看,JDK7下預設的48M可以設大點,不摳這麼點。

 

3. 監控篇

JVM輸出的各種日誌,如果未指定路徑,通常會生成到執行應用的相同目錄,為了避免有時候在不同的地方執行啟動指令碼,一般將日誌路徑集中設到一個固定的地方。

3.1 監控建議配置

1. -XX:+PrintCommandLineFlags

運維有時會對啟動引數做一些臨時的更改,將每次啟動的引數輸出到stdout,將來有據可查。
打印出來的是命令列裡設定了的引數以及因為這些引數隱式影響的引數,比如開了CMS後,-XX:+UseParNewGC也被自動開啟。
 

2. -XX:-OmitStackTraceInFastThrow

為異常設定StackTrace是個昂貴的操作,所以當應用在相同地方丟擲相同的異常N次(兩萬?)之後,JVM會對某些特定異常如NPE,陣列越界等進行優化,不再帶上異常棧。此時,你可能會看到日誌裡一條條Nul Point Exception,而之前輸出完整棧的日誌早被滾動到不知哪裡去了,也就完全不知道這NPE發生在什麼地方,欲哭無淚。 所以,將它禁止吧,ElasticSearch也這樣幹。

 

3.2 Crash檔案

1. -XX:ErrorFile

JVM crash時,hotspot 會生成一個error檔案,提供JVM狀態資訊的細節。如前所述,將其輸出到固定目錄,避免到時會到處找這檔案。檔名中的%p會被自動替換為應用的PID

-XX:ErrorFile=${MYLOGDIR}/hs_err_%p.log

 
2. coredump

當然,更好的做法是生成coredump,從CoreDump能夠轉出Heap Dump 和 Thread Dump 還有crash的地方,非常實用。

在啟動腳本里加上 ulimit -c unlimited或其他的設定方式,如果有root許可權,設一下輸出目錄更好

echo "/{MYLOGDIR}/coredump.%p" > /proc/sys/kernel/core_pattern

什麼?你不知道coredump有什麼用?看來你是沒遇過JVM Segment Fault的幸福人。
 

3. -XX:+HeapDumpOnOutOfMemoryError(可選)

在Out Of Memory,JVM快死掉的時候,輸出Heap Dump到指定檔案。不然開發很多時候還真不知道怎麼重現錯誤。

路徑只指向目錄,JVM會保持檔名的唯一性,叫java_pid${pid}.hprof。因為如果指向檔案,而檔案已存在,反而不能寫入。

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGDIR}/

但在容器環境下,輸出4G的HeapDump,在普通硬碟上會造成20秒以上的硬碟IO跑滿,也是個十足的惡鄰,影響了同一宿主機上所有其他的容器。

 

3.3 GC日誌

JDK9完全不一樣了,這裡還是寫JDK7/8的配置。

1.基本配置

-Xloggc:/dev/shm/gc-myapp.log -XX:+PrintGCDateStamps -XX:+PrintGCDetails

有人擔心寫GC日誌會影響效能,但測試下來實在沒什麼影響,GC問題是Java裡最常見的問題,沒日誌怎麼行。

後來又發現如果遇上高IO的情況,GC時作業系統正在flush pageCache 到磁碟,也可能導致GC log檔案被鎖住,從而讓GC結束不了。所以把它指向了/dev/shm 這種記憶體中檔案系統,避免這種停頓,詳見Eliminating Large JVM GC Pauses Caused by Background IO Traffic

用PrintGCDateStamps而不是PrintGCTimeStamps,列印可讀的日期而不是時間戳。

 
2. -XX:+PrintGCApplicationStoppedTime

這是個非常非常重要的引數,但它的名字沒起好,其實除了列印清晰的完整的GC停頓時間外,還可以列印其他的JVM停頓時間,比如取消偏向鎖,class 被agent redefine,code deoptimization等等,有助於發現一些原來沒想到的問題。如果真的發現了一些不知是什麼的停頓,需要列印安全點日誌找原因(見後)。

 
3. -XX:+PrintGCCause

列印產生GC的原因,比如AllocationFailure什麼的,在JDK8已預設開啟,JDK7要顯式開啟一下。

 
4. -XX:+PrintPromotionFailure

打開了就知道是多大的新生代物件晉升到老生代失敗從而引發Full GC時的。

 
5. GC日誌滾動與備份

GC日誌預設會在重啟後清空,有人擔心長期執行的應用會把檔案弄得很大,所以"-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=1M"的引數可以讓日誌滾動起來。但真正用起來重啟後的檔名太混亂太讓人頭痛,GC日誌再大也達不到哪裡去,所以我們沒有加滾動,而且自行在啟動腳本里對舊日誌做備份。

 

3.4 安全點日誌

如果GC日誌裡有非GC的JVM停頓時間,你得打出安全點日誌來知道詳情,詳見 JVM的Stop The World,安全點,黑暗的地底世界

-XX:+PrintSafepointStatistics -XX: PrintSafepointStatisticsCount=1 -XX:+UnlockDiagnosticVMOptions -XX:- DisplayVMOutput -XX:+LogVMOutput -XX:LogFile=/dev/shm/vm-myapp.log

3.5 JMX

 

-Dcom.sun.management.jmxremote.port=7001 -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -Djava.rmi.server.hostname=127.0.0.1

以上設定,只讓本地的Zabbix之類監控軟體通過JMX監控JVM,不允許遠端訪問。

如果應用忘記了加上述引數,又不想改引數重啟服務,可以用VJTools的vjmxcli來救急,它能通過PID直接連入目標JVM開啟JMX。

原文地址:http://calvin1978.blogcn.com/articles/jvmoption-7.html  江南白衣