JVM效能調優牽扯到各方面的取捨與平衡,往往是牽一髮而動全身,需要全盤考慮各方面的影響。在優化時候,切勿憑感覺或經驗主義進行調整,而是需要通過系統執行的客觀資料指標,不斷找到最優解。同時,在進行效能調優前,您需要理解並掌握以下的相關基礎理論知識:
1、JVM垃圾收集器和垃圾回收演演算法
2、JVM效能監控常用工具和命令
3、JVM執行時資料區域
4、能夠讀懂gc紀錄檔
5、記憶體分配與回收策略
從上圖可以看出,整個JVM記憶體是由棧記憶體、堆記憶體和永久代構成。
年輕代(New generation) = eden + s0 + s1
堆記憶體 = 年輕代 + 老年代(Old generation)
JDK1.8以前: JVM記憶體 = 棧記憶體 + 堆記憶體 + 永久代
JDK1.8以後: 由元空間取代了永久代,元空間並不在JVM中,而是使用本地記憶體。因此JVM記憶體 = 棧記憶體 + 堆記憶體
棧記憶體歸屬於單個執行緒,也就是每建立一個執行緒都會分配一塊棧記憶體,而棧中儲存的東西只有本執行緒可見,屬於執行緒私有。
棧的生命週期與執行緒一致,一旦執行緒結束,棧記憶體也就被回收。
棧中存放的內容主要包括:8大基本型別 + 物件的參照 + 範例的方法
堆記憶體是由年輕代和老年代構成,JDK1.8以後,永久代被元空間取代,使用直接記憶體,不佔用堆記憶體。堆記憶體是Jvm中空間最大的區域,所有執行緒共用堆,所有的陣列以及記憶體物件的範例都在此區域分配。我們常說的垃圾回收就是作用於堆記憶體。
Eden區佔大容量,Survivor兩個區佔小容量,預設比例是8:1:1
這個區域是常駐記憶體的。用來存放JDK自身攜帶的Class物件。Interface後設資料,儲存的是Java執行時的一些環境。這個區域不存在垃圾回收!關閉虛擬機器器就會釋放這個區域的記憶體。
當發現系統中元空間佔用記憶體比較大時,排查方向是否載入了大量的第三方jar包,Tomcat部署了太多應用,大量動態生成的反射類等。
首先JVM記憶體限制於實際的最大實體記憶體,假設實體記憶體無限大的話,JVM記憶體的最大值跟作業系統有很大的關係。簡單的說就32位元處理器雖然可控記憶體空間有4GB,但是具體的作業系統會給一個限制,這個限制一般是2GB-3GB(一般來說Windows系統下為1.5G-2G,Linux系統下為2G-3G),而64bit以上的處理器就不會有限制。
java -server -Xmx4g -Xms4g -Xmn2g –Xss128k
-Xmx4g:設定JVM最大可用記憶體為4g。
-Xms4g:設定JVM最小可用記憶體為4g。一般設定為與-Xmx相同,避免每次垃圾回收完成後JVM重新分配記憶體。
-Xmn2g:設定年輕代大小為2G。整個堆大小=年輕代大小 + 年老代大小,所以增大年輕代後,將會減小年老代大小。
-Xss128k:設定每個執行緒的堆疊大小。JDK5.0以後每個執行緒預設大小為1M,以前每個執行緒大小為256K。根據應用的執行緒所需記憶體大小進行調整。在相同實體記憶體下,減小這個值能生成更多的執行緒。
java -server -Xmx4g -Xms4g -Xmn2g –Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxMetaspaceSize=16m -XX:MaxTenuringThreshold=0
-XX:NewRatio=4: 設定年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代)。設定為4,則年輕代與年老代所佔比值為1:4,年輕代佔整個堆疊的1/5
-XX:SurvivorRatio=4: 設定年輕代中Eden區與Survivor區的大小比值。設定為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區佔整個年輕代的1/6
-XX:MaxMetaspaceSize=16m: 設定元空間最大可分配大小為16m。
-XX:MaxTenuringThreshold=0: 設定垃圾最大年齡。如果設定為0的話,則年輕代物件不經過Survivor區,直接進入年老代。對於年老代比較多的應用,可以提高效率。如果將此值設定為一個較大值,則年輕代物件會在Survivor區進行多次複製,這樣可以增加物件再年輕代的存活時間,增加在年輕代即被回收的概率。
JVM給了三種選擇:序列收集器、並行收集器、並行收集器,但是序列收集器只適用於小資料量的情況,所以這裡的選擇主要針對並行收集器和並行收集器。預設情況下,JDK5.0以前都是使用序列收集器,如果想使用其他收集器需要在啟動時加入相應引數。JDK5.0以後,JVM會根據當前系統設定進行判斷。
java -server -Xmx4g -Xms4g -Xmn2g –Xss128k -XX:+UseParallelGC -XX:ParallelGCThreads=20 -XX:+UseParallelOldGC -XX:+UseAdaptiveSizePolicy
-XX:+UseParallelGC:選擇垃圾收集器為並行收集器。此設定僅對年輕代有效。即上述設定下,年輕代使用並行收集,而年老代仍舊使用序列收集。
-XX:ParallelGCThreads=20:設定並行收集器的執行緒數,即:同時多少個執行緒一起進行垃圾回收。此值最好設定與處理器數目相等。
-XX:+UseParallelOldGC:設定年老代垃圾收集方式為並行收集。JDK6.0支援對年老代並行收集。
-XX:+UseAdaptiveSizePolicy:設定此選項後,並行收集器會自動選擇年輕代區大小和相應的Survivor區比例,以達到目標系統規定的最低相應時間或者收集頻率等,此值建議使用並行收集器時,一直開啟。
java -server -Xmx4g -Xms4g -Xmn2g –Xss128k -XX:ParallelGCThreads=20 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection
-XX:+UseConcMarkSweepGC: 設定年老代為並行收集
-XX:+UseParNewGC: 設定年輕代為並行收集。可與CMS收集同時使用
-XX:CMSFullGCsBeforeCompaction:由於並行收集器不對記憶體空間進行壓縮、整理,所以執行一段時間以後會產生「碎片」,使得執行效率降低。此值設定執行多少次GC以後對記憶體空間進行壓縮、整理。
-XX:+UseCMSCompactAtFullCollection:開啟對年老代的壓縮。可能會影響效能,但是可以消除碎片
GC紀錄檔列印
-XX:+PrintGC:輸出形式:[GC 118250K->113543K(130112K), 0.0094143 secs] [Full GC 121376K->10414K(130112K), 0.0650971 secs]
-XX:+PrintGCDetails:輸出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs] [GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]
OOM生成dump檔案
-XX:+HeapDumpOnOutOfMemoryError 表示jvm發生oom異常時,自動生成dump檔案
-XX:HeapDumpPath= 表示生成dump檔案的存放目錄
一般來說記憶體溢位主要分為以下幾類:
堆溢位(java.lang.OutOfMemoryError: Java heap space)
棧深度不夠( java.lang.StackOverflowError)
棧執行緒數不夠(java.lang.OutOfMemoryError: unable to create new native thread)
元空間溢位(java.lang.OutOfMemoryError: Metaspace)
Metaspace元空間主要是儲存類的後設資料資訊,各種類描述資訊,比如類名、屬性、方法、存取限制等,按照一定的結構儲存在Metaspace裡。
一般來說,元空間大小是固定不變的。在出現溢位後,首先通過命令或監控工具(如下圖)檢視元空間大小,再檢查是否-XX:MaxMetaspaceSize設定太小導致。
如果發現元空間大小是持續上漲的,則需要檢查程式碼是否存在大量的反射類載入、動態代理生成的類載入等導致。可以通過-XX:+TraceClassLoading -XX:+TraceClassUnloading記錄下類的載入和解除安裝情況,反推具體問題程式碼。
引發StackOverFlowError的常見原因有:
/**
* VM Args: -Xss128k
*/
public class JavaStackSOF {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaStackSOF oom = new JavaStackSOF();
try{
oom.stackLeak();
}catch(Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
}
}
}
stack length:2101
Exception in thread "main" java.lang.StackOverflowError
at com.sandy.jvm.chapter02.JavaStackSOF.stackLeak(JavaStackSOF.java:13)
at com.sandy.jvm.chapter02.JavaStackSOF.stackLeak(JavaStackSOF.java:14)
at com.sandy.jvm.chapter02.JavaStackSOF.stackLeak(JavaStackSOF.java:14)
這類錯誤目前在生成系統只遇到過一次,原因是:linux系統中非root使用者預設建立執行緒數最多是1024。解決辦法是修改檔案:/etc/security/limits.d/90-nproc.conf
還有一種情況是-xss設定太大,那麼作業系統可建立的最大執行緒數太小導致,一般除非誤操作是不會出現此問題的。
堆溢位是常見也是最複雜的一種情況。導致堆溢位可能的情況有:
解決思路一般是:
一、堆dump檔案獲取
1、通過引數設定自動獲取dump檔案(推薦)
2、jmap -dump:format=b,file=filename.hprof pid
二、MAT工具分析
1、分析大物件、堆中儲存資訊、可能存在的記憶體漏失地方,便於定位問題位置
常用的監控工具或命令有:jstack、jstat、jConsole、jvisualvm。監控指標主要是各記憶體區域大小是否合理、fullGC頻率及耗時、youngGC耗時、執行緒數等。
jstack主要用於列印執行緒堆疊資訊,幫助問題的定位。一般配合top -Hp PID使用。
通過top命令發現某個java服務佔用1234%的CPU,如圖:
通過top -Hp PID命令可以看到佔用CPU比較高的執行緒,如圖:
再次通過jstack PID>log.txt,輸出堆疊資訊即可進行排查定位。
jstat命令是分析JVM執行狀況的常用命令。
jstat -options
-class 用於檢視類載入情況的統計
-compiler 用於檢視HotSpot中即時編譯器編譯情況的統計
-gc 用於檢視JVM中堆的垃圾收集情況的統計
-gccapacity 用於檢視新生代、老生代及持久代的儲存容量情況
-gcmetacapacity 顯示metaspace的大小
-gcnew 用於檢視新生代垃圾收集的情況
-gcnewcapacity 用於檢視新生代儲存容量的情況
-gcold 用於檢視老生代及持久代垃圾收集的情況
-gcoldcapacity 用於檢視老生代的容量
-gcutil 顯示垃圾收集資訊
-gccause 顯示垃圾回收的相關資訊(通-gcutil),同時顯示最後一次僅當前正在發生的垃圾收集的原因
-printcompilation 輸出JIT編譯的方法資訊
以jstat -gcutil為例:
[root@hadoop ~]# jstat -gcutil 3346 #顯示垃圾收集資訊
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
52.97 0.00 42.10 13.92 97.39 98.02 8 0.020 0 0.000 0.020
JConsole是基於JMX的視覺化監視、管理工具。可以很方便的監視本地及遠端伺服器的java程序的記憶體使用情況。
無需認證的遠端監控設定
-Dcom.sun.management.jmxremote.port=60001 //監控的埠號
-Dcom.sun.management.jmxremote.authenticate=false //關閉認證
-Dcom.sun.management.jmxremote.ssl=false
-Djava.rmi.server.hostname=192.168.1.2
找到 JDK 安裝路徑,開啟bin資料夾,雙擊jconsole.exe,在已經開啟的JConsole介面操作「連線->新建連線->選擇遠端程序->輸入遠端主機IP和埠號->點選「連線
jvisualvm與jConsole連線方式一致,連線後介面如下: