1. 程式人生 > >關鍵業務系統的JVM引數推薦

關鍵業務系統的JVM引數推薦

轉載自:http://calvin1978.blogcn.com/articles/jvmoption-7.html

效能篇

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%的影響。

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

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

4. -XX:-UseCounterDecay

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

3. -XX:-TieredCompilation

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

記憶體篇

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

1. CMS基本寫法

  -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly。為了讓CMSInitiatingOccupancyFraction這個設定生效,還要設定-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演算法,不是全程停頓,必選。

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

6. 併發收集執行緒數

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 核數的,通常弄巧成拙。

7. -XX:-CMSClassUnloadingEnabled

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

8. -XX:+CMSScavengeBeforeRemark

  ​預設為關閉,在CMS remark前,先執行一次minor GC將新生代清掉,這樣從老生代的物件引用到的新生代物件的個數就少了,停止全世界的CMS remark階段就短一些。如果打開了,會讓一次YGC緊接著一次CMS GC,使得停頓的總時間加長了。
  ​又一個真實案例,CMS GC的時間和當時新生代的大小成比例,新生代很小時很快完成,新生代80%時CMS GC停頓時間超過一秒,這時候就還是打開了划算。

9. -XX:CMSFullGCsBeforeCompaction

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

10. -Xverify:none

  ​來自優化Eclipse啟動速度的經驗,說關閉Java類載入驗證可以加快10% -15%的啟動速度。

監控篇

1. -XX:+PrintCommandLineFlags

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

2. -XX:-OmitStackTraceInFastThrow

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

3. -XX:ErrorFile

  ​JVM crash時,hotspot 會生成一個error檔案,提供JVM狀態資訊的細節。如前所述,將其輸出到固定目錄,避免到時會到處找這檔案。檔名中的%p會被自動替換為應用的PID:-XX:ErrorFile=${MYLOGDIR}/hs_err_%p.log

4. -XX:+HeapDumpOnOutOfMemoryError(可選)

  ​在Out Of Memory,JVM快死掉的時候,輸出Heap Dump到指定檔案。不然開發很多時候還真不知道怎麼重現錯誤。
  ​路徑只指向目錄,JVM會保持檔名的唯一性,叫java_pid${pid}.hprof。因為如果指向檔案,而檔案已存在,反而不能寫入。
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGDIR}/
  ​但在容器環境下,輸出4G的HeapDump,在普通硬碟上會造成20秒以上的硬碟IO跑滿,也是個十足的惡鄰,影響了同一宿主機上所有其他的容器。

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

  ​有人擔心寫GC日誌會影響效能,但測試下來實在沒什麼影響,GC問題是Java裡最常見的問題,沒日誌怎麼行。
  ​後來又發現如果遇上高IO的情況,GC時作業系統正在flush pageCache 到磁碟,也可能導致GC log檔案被鎖住,從而讓GC結束不了。所以把它指向了/dev/shm 這種記憶體中檔案系統,避免這種停頓。
  ​用PrintGCDateStamps而不是PrintGCTimeStamps,列印可讀的日期而不是時間戳。

6. -XX:+PrintGCApplicationStoppedTime

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

7. -XX:+PrintGCCause

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

8. -XX:+PrintPromotionFailure

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

9. GC日誌滾動與備份

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

10. 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,不允許遠端訪問。