JVM參數優化(基礎篇)

這幾天壓測預生產環境,發現TPS各種不穩。因爲是重構的系統,據說原來的系統在高併發的時候一點問題沒有,結果重構的系統被幾十個併發壓一下就各種不穩定。雖然測試的同事沒有說啥,但自己感覺被啪啪的打臉。。。

於是各種排查,最先想到的就是JVM參數,於是優化一番,希望能夠出一個好的結果。儘管後來證明不穩定的原因是安裝LoadRunner的壓測服務器不穩定,不關我的系統的事,不過也是記錄一下,一是做個備份,二是可以給別人做個參考。

寫在前面的話

因爲Hotspot JDK提供的參數默認值,在小版本之間不斷變化,參數之間也會互相影響。而且,服務器配置不同,都可能影響最後的效果。所以千萬不要迷信網上的某篇文章(包括這篇)裏面的參數配置,一切的配置都需要自己親身測試一番才能用。針對於JVM參數默認值不斷變化,可以使用-XX:+PrintFlagsFinal打印當前環境JVM參數默認值,比如:java -XX:PrintFlagsFinal -version,也可以用java [生產環境參數] -XX:+PrintFlagsFinal –version | grep [待查證的參數]查看具體的參數數據。

這裏是一個8G服務器的參數,JDK版本信息如下:

java version "1.8.0_73"
Java(TM) SE Runtime Environment (build 1.8.0_73-b02)
Java HotSpot(TM) 64-Bit Server VM (build 25.73-b02, mixed mode)

堆設置

堆內存設置應該算是一個Java程序猿的基本素養,最少也得修改過Xms、Xmx、Xmn這三個參數了。但是一個2G堆大小的JVM,可能總共佔用多少內存的?

堆內存 + 線程數 * 線程棧 + 永久代 + 二進制代碼 + 堆外內存

2G + 1000 * 1M + 256M + 48/240M + (~2G) = 5.5G (3.5G)
- 堆內存: 存儲Java對象,默認爲物理內存的1/64
- 線程棧: 存儲局部變量(原子類型,引用)及其他,默認爲1M
- 永久代: 存儲類定義及常量池,注意JDK7/8的區別
- 二進制代碼:JDK7與8,打開多層編譯時的默認值不一樣,從48到240M
- 堆外內存: 被Netty,堆外緩存等使用,默認最大值約爲堆內存大小

也就是說,堆內存設置爲2G,那一個有1000個線程的JVM可能需要佔5.5G,在考慮系統佔用、IO佔用等等各種情況,一臺8G的服務器,也就啓動一個服務了。當然,如果線程數少、併發不高、壓力不大,還是可以啓動多個,而且也可以把堆內存降低。

  1. -Xms2g 與 -Xmx2g:堆內存大小,第一個是最小堆內存,第二個是最大堆內存,比較合適的數值是2-4g,再大就得考慮GC時間
  2. -Xmn1g 或 (-XX:NewSize=1g 和 -XX:MaxNewSize=1g) 或 -XX:NewRatio=1:設置新生代大小,JDK默認新生代佔堆內存大小的1/3,也就是-XX:NewRatio=2。這裏是設置的1g,也就是-XX:NewRatio=1。可以根據自己的需要設置。
  3. -XX:MetaspaceSize=128m 和 -XX:MaxMetaspaceSize=512m,JDK8的永生代幾乎可用完機器的所有內存,爲了保護服務器不會因爲內存佔用過大無法連接,需要設置一個128M的初始值,512M的最大值保護一下。
  4. -XX:SurvivorRatio:新生代中每個存活區的大小,默認爲8,即1/10的新生代, 1/(SurvivorRatio+2),有人喜歡設小點省點給新生代,但要避免太小使得存活區放不下臨時對象而要晉升到老生代,還是從GC Log裏看實際情況了。
  5. -Xss256k:在堆之外,線程佔用棧內存,默認每條線程爲1M。存放方法調用出參入參的棧、局部變量、標量替換後的局部變量等,有人喜歡設小點節約內存開更多線程。但反正內存夠也就不必要設小,有人喜歡再設大點,特別是有JSON解析之類的遞歸調用時不能設太小。
  6. -XX:MaxDirectMemorySize:堆外內存/直接內存的大小,默認爲堆內存減去一個Survivor區的大小。
  7. -XX:ReservedCodeCacheSize:JIT編譯後二進制代碼的存放區,滿了之後就不再編譯。默認開多層編譯240M,可以在JMX裏看看CodeCache的大小。

GC設置

目前比較主流的GC是CMS和G1,有大神建議以8G爲界。(據說JDK 9默認的是G1)。因爲應用設置的內存都比較小,所以選擇CMS收集器。下面的參數也是針對CMS收集器的,等之後如果有需要,再補充G1收集器的參數。

CMS設置

  1. -XX:+UseConcMarkSweepGC:啓用CMS垃圾收集器
  2. -XX:CMSInitiatingOccupancyFraction=80 與 -XX:+UseCMSInitiatingOccupancyOnly:兩個參數需要配合使用,否則第一個參數的75只是一個參考值,JVM會重新計算GC的時間。
  3. -XX:MaxTenuringThreshold=15:對象在Survivor區熬過多少次Young GC後晉升到年老代,默認是15。Young GC是最大的應用停頓來源,而新生代裏GC後存活對象的多少又直接影響停頓的時間,所以如果清楚Young GC的執行頻率和應用裏大部分臨時對象的最長生命週期,可以把它設的更短一點,讓其實不是臨時對象的新生代長期對象趕緊晉升到年老代。
  4. -XX:-DisableExplicitGC:允許使用System.gc()主動調用GC。這裏需要說明下,有的JVM優化建議是設置-XX:-DisableExplicitGC,關閉手動調用System.gc()。這是應爲System.gc()是觸發Full GC,頻繁的Full GC會嚴重影響性能。但是很多NIO框架,比如Netty,會使用堆外內存,如果沒有Full GC的話,堆外內存就無法回收。如果不主動調用System.gc(),就需要等到JVM自己觸發Full GC,這個時候,就可能引起長時間的停頓(STW),而且機器負載也會升高。所以不能夠完全禁止System.gc(),又得縮短Full GC的時間,那就使用-XX:+ExplicitGCInvokesConcurrent-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses選項,使用CMS收集器來觸發Full GC。這兩個選項需要配合-XX:+UseConcMarkSweepGC使用。
  5. -XX:+ExplicitGCInvokesConcurrent:使用System.gc()時觸發CMS GC,而不是Full GC。默認是不開啓的,只有使用-XX:+UseConcMarkSweepGC選項的時候才能開啓這個選項。
  6. -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses:使用System.gc()時,永久代也被包括進CMS範圍內。只有使用-XX:+UseConcMarkSweepGC選項的時候才能開啓這個選項。
  7. -XX:+ParallelRefProcEnabled:默認爲false,並行的處理Reference對象,如WeakReference,除非在GC log裏出現Reference處理時間較長的日誌,否則效果不會很明顯。
  8. -XX:+ScavengeBeforeFullGC:在Full GC執行前先執行一次Young GC。
  9. -XX:+UseGCOverheadLimit: 限制GC的運行時間。如果GC耗時過長,就拋OOM。
  10. -XX:+UseParallelGC:設置並行垃圾收集器
  11. -XX:+UseParallelOldGC:設置老年代使用並行垃圾收集器
  12. -XX:-UseSerialGC:關閉串行垃圾收集器
  13. -XX:+CMSParallelInitialMarkEnabled 和 -XX:+CMSParallelRemarkEnabled:降低標記停頓
  14. -XX:+CMSScavengeBeforeRemark:默認爲關閉,在CMS remark前,先執行一次minor GC將新生代清掉,這樣從老生代的對象引用到的新生代對象的個數就少了,停止全世界的CMS remark階段就短一些。如果看到GC日誌裏remark階段的時間超長,可以打開此項看看有沒有效果,否則還是不要打開了,白白多了次YGC。
  15. -XX:CMSWaitDuration=10000:設置垃圾收集的最大時間間隔,默認是2000。
  16. -XX:+CMSClassUnloadingEnabled:在CMS中清理永久代中的過期的Class而不等到Full GC,JDK7默認關閉而JDK8打開。看自己情況,比如有沒有運行動態語言腳本如Groovy產生大量的臨時類。它會增加CMS remark的暫停時間,所以如果新類加載並不頻繁,這個參數還是不開的好。

GC日誌

GC過程可以通過GC日誌來提供優化依據。

  1. -XX:+PrintGCDetails:啓用gc日誌打印功能
  2. -Xloggc:/path/to/gc.log:指定gc日誌位置
  3. -XX:+PrintHeapAtGC:打印GC前後的詳細堆棧信息
  4. -XX:+PrintGCDateStamps:打印可讀的日期而不是時間戳
  5. -XX:+PrintGCApplicationStoppedTime:打印所有引起JVM停頓時間,如果真的發現了一些不知什麼的停頓,再臨時加上-XX:+PrintSafepointStatistics -XX: PrintSafepointStatisticsCount=1找原因。
  6. -XX:+PrintGCApplicationConcurrentTime:打印JVM在兩次停頓之間正常運行時間,與-XX:+PrintGCApplicationStoppedTime一起使用效果更佳。
  7. -XX:+PrintTenuringDistribution:查看每次minor GC後新的存活週期的閾值
  8. -XX:+UseGCLogFileRotation 與 -XX:NumberOfGCLogFiles=10 與 -XX:GCLogFileSize=10M:GC日誌在重啓之後會清空,但是如果一個應用長時間不重啓,那GC日誌會增加,所以添加這3個參數,是GC日誌滾動寫入文件,但是如果重啓,可能名字會出現混亂。
  9. -XX:PrintFLSStatistics=1:打印每次GC前後內存碎片的統計信息

其他參數設置

  1. -ea:啓用斷言,這個沒有什麼好說的,可以選擇啓用,或這選擇不啓用,沒有什麼大的差異。完全根據自己的系統進行處理。
  2. -XX:+UseThreadPriorities:啓用線程優先級,主要是因爲我們可以給予週期性任務更低的優先級,以避免干擾客戶端工作。在我當前的環境中,是默認啓用的。
  3. -XX:ThreadPriorityPolicy=42:允許降低線程優先級
  4. -XX:+HeapDumpOnOutOfMemoryError:發生內存溢出是進行heap-dump
  5. -XX:HeapDumpPath=/path/to/java_pid.hprof:這個參數與-XX:+HeapDumpOnOutOfMemoryError共同作用,設置heap-dump時內容輸出文件。
  6. -XX:ErrorFile=/path/to/hs_err_pid.log:指定致命錯誤日誌位置。一般在JVM發生致命錯誤時會輸出類似hs_err_pid.log的文件,默認是在工作目錄中(如果沒有權限,會嘗試在/tmp中創建),不過還是自己指定位置更好一些,便於收集和查找,避免丟失。
  7. -XX:StringTableSize=1000003:指定字符串常量池大小,默認值是60013。對Java稍微有點常識的應該知道,字符串是常量,創建之後就不可修改了,這些常量所在的地方叫做字符串常量池。如果自己系統中有很多字符串的操作,且這些字符串值比較固定,在允許的情況下,可以適當調大一些池子大小。
  8. -XX:+AlwaysPreTouch:在啓動時把所有參數定義的內存全部捋一遍。使用這個參數可能會使啓動變慢,但是在後面內存使用過程中會更快。可以保證內存頁面連續分配,新生代晉升時不會因爲申請內存頁面使GC停頓加長。通常只有在內存大於32G的時候纔會有感覺。
  9. -XX:-UseBiasedLocking:禁用偏向鎖(在存在大量鎖對象的創建且高度併發的環境下(即非多線程高併發應用)禁用偏向鎖能夠帶來一定的性能優化)
  10. -XX:AutoBoxCacheMax=20000:增加數字對象自動裝箱的範圍,JDK默認-128~127的int和long,超出範圍就會即時創建對象,所以,增加範圍可以提高性能,但是也是需要測試。
  11. -XX:-OmitStackTraceInFastThrow:不忽略重複異常的棧,這是JDK的優化,大量重複的JDK異常不再打印其StackTrace。但是如果系統是長時間不重啓的系統,在同一個地方跑了N多次異常,結果就被JDK忽略了,那豈不是查看日誌的時候就看不到具體的StackTrace,那還怎麼調試,所以還是關了的好。
  12. -XX:+PerfDisableSharedMem:啓用標準內存使用。JVM控制分爲標準或共享內存,區別在於一個是在JVM內存中,一個是生成/tmp/hsperfdata_{userid}/{pid}文件,存儲統計數據,通過mmap映射到內存中,別的進程可以通過文件訪問內容。通過這個參數,可以禁止JVM寫在文件中寫統計數據,代價就是jps、jstat這些命令用不了了,只能通過jmx獲取數據。但是在問題排查是,jps、jstat這些小工具是很好用的,比jmx這種很重的東西好用很多,所以需要自己取捨。這裏有個GC停頓的例子。
  13. -Djava.net.preferIPv4Stack=true:這個參數是屬於網絡問題的一個參數,可以根據需要設置。在某些開啓ipv6的機器中,通過InetAddress.getLocalHost().getHostName()可以獲取完整的機器名,但是在ipv4的機器中,可能通過這個方法獲取的機器名不完整,可以通過這個參數來獲取完整機器名。

大神給出的例子

下面貼上大神給出的例子,可以參考使用,不過還是建議在自己的環境中有針對的驗證之後再使用,畢竟大神的環境和自己的環境還是不同。

性能相關

-XX:-UseBiasedLocking -XX:-UseCounterDecay -XX:AutoBoxCacheMax=20000
-XX:+PerfDisableSharedMem(可選) -XX:+AlwaysPreTouch -Djava.security.egd=file:/dev/./urandom

內存大小相關(JDK7)

-Xms4096m -Xmx4096m -Xmn2048m -XX:MaxDirectMemorySize=4096m
-XX:PermSize=128m -XX:MaxPermSize=512m -XX:ReservedCodeCacheSize=240M

如果使用jdk8,就把-XX:PermSize=128m -XX:MaxPermSize=512m換成-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m,正如前面所說的,這兩套參數是爲了保證安全的,建議還是加上。

CMS GC相關

-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly -XX:MaxTenuringThreshold=6
-XX:+ExplicitGCInvokesConcurrent -XX:+ParallelRefProcEnabled

GC日誌相關

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

異常日誌相關

-XX:-OmitStackTraceInFastThrow -XX:ErrorFile=${LOGDIR}/hs_err_%p.log
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOGDIR}/

JMX相關

-Dcom.sun.management.jmxremote.port=${JMX_PORT} -Dcom.sun.management.jmxremote
-Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.authenticate=false
-Dcom.sun.management.jmxremote.ssl=false

參考文章

  1. Java性能優化指南1.8版,及唯品會的實戰
  2. Java中的逃逸分析和TLAB以及Java對象分配
  3. The Four Month Bug: JVM statistics cause garbage collection pauses

個人主頁: http://www.howardliu.cn

個人博文: JVM參數優化(基礎篇)

CSDN主頁: http://blog.csdn.net/liuxinghao

CSDN博文: JVM參數優化(基礎篇)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章