1. 程式人生 > >JVM效能調優實踐——JVM篇

JVM效能調優實踐——JVM篇

前言

在遇到實際效能問題時,除了關注系統性能指標。還要結合應用程式的系統的日誌、堆疊資訊、GClog、threaddump等資料進行問題分析和定位。關於效能指標分析可以參考前一篇JVM效能調優實踐——效能指標分析

JVM的調優和故障處理可以使用JDK的幾個常用命令工具。因為本文是基於Docker容器內部的Springboot服務。需要調整一下docker容器的啟動引數,才可以使用jmap等工具。jmap命令需要使用Linux的Capability的PTRACE_ATTACH許可權。而Docker自1.10在預設的seccomp配置檔案中禁用了PTRACE_ATTACH。目前使用的Docker version是17.04.0-ce。支援的Capability列表可以詳看

runtime-privilege-and-linux-capabilities
調整Capability的方式也比較方便。可以如下直接在執行引數後面加 cap_add,cap-drop

$docker run --cap-add=ALL --cap-drop=MKNOD ...

也可以在compose中增加:

   cap_add:
     - ALL
   cap_drop:
     - NET_ADMIN
     - SYS_ADMIN

Docker容器中的服務程序

在排查問題時,一般是先通過JVM效能調優實踐——效能指標分析中的幾個命令來分析基礎的伺服器狀態和資訊。在微服務架構中,每臺伺服器部署著若干執行著服務的容器。在不能通過應用日誌或者問題現象定位問題服務時,需要找到問題容器。
先通過TOP

命令找到耗費關鍵資源的程序。

top - 11:45:13 up 318 days, 20:43,  2 users,  load average: 0.15, 0.19, 0.18
Tasks: 172 total,   1 running, 171 sleeping,   0 stopped,   0 zombie
%Cpu(s):  3.1 us,  1.9 sy,  0.0 ni, 94.7 id,  0.0 wa,  0.0 hi,  0.3 si,  0.0 st
KiB Mem:   8175392 total,  7868636 used,   306756 free,   204400 buffers
KiB Swap: 0 total, 0 used, 0 free. 849564 cached Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 31399 root 20 0 3585612 806804 12228 S 3.0 9.9 548:20.94 java 6331 root 20 0 3445612 925660 15784 S 2.7 11.3 41:40.29 java 31122 root 20 0 3460712 888776 11568 S 2.0 10.9 484:19.31 java 31147 root 20 0 3288180 811476 12748 S 1.3 9.9 263:44.73 java 8506 root 20 0 3254088 750880 6116 S 1.0 9.2 760:45.19 java 22940 root 20 0 1029012 70584 23396 S 0.7 0.9 0:10.68 node 24550 root 20 0 1229088 43096 8712 S 0.7 0.5 160:15.74 node 7 root 20 0 0 0 0 S 0.3 0.0 606:49.74 rcu_sched 454 sshd 20 0 32792 1924 188 S 0.3 0.0 29:15.40 nginx 13721 root 20 0 25396 1956 1324 S 0.3 0.0 56:29.17 AliYunDunUpdate 16225 root 20 0 3072752 429296 6848 S 0.3 5.3 42:51.01 java 20795 root 20 0 2408848 75344 3960 S 0.3 0.9 2361:22 java 23581 root 20 0 16736 2676 2196 R 0.3 0.0 0:00.01 top 31352 root 20 0 206920 1488 1024 S 0.3 0.0 1:20.48 docker-containe 32000 root 20 0 3061760 403708 6548 S 0.3 4.9 127:01.39 java ... 省略其他資訊

因為Docker容器中還有java程序,所以需要找到具體的父子程序id.用ps -ef命令如下所示。第二列是PID(程序ID),第三列是PPID(父程序ID)。

$ps -ef |grep java 
root      6310  6293  0 May21 ?        00:00:00 /bin/sh -c java -Dcontainer.host.ip=...
root      6331  6310  2 May21 ?        00:41:51 java -Dcontainer.host.ip= -server ... 
root      8482  8465  0 Apr16 ?        00:00:00 /bin/sh -c java -Dcontainer.host.ip...
root      8506  8482  1 Apr16 ?        12:40:53 java -Dcontainer.host.ip= -server...
... 省略其他資訊

可以使用docker inspect檢視容器內部資訊,找到對應的容器例項的程序資訊。如下即可列印當前宿主機的所有執行的容器例項的PID,為了方便對映,可以列印對應容器名字,或者容器ID:

## 列印容器pid和容器id
$docker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.ID}}' | grep "^${PID}"
## 列印容器pid和容器name 
$ docker ps -q | xargs docker inspect --format '{{.State.Pid}}, {{.Name}}' | grep "^${PID}"  
6310, /service-item
31369, /gateway-api
31094, /service-resource
31025, /service-trade
30916, /service-user
16204, /service-analytics
8482, /service-financial
... 省略其他資訊

如果要分析最消耗記憶體的程序,對應的pid= 6331,其所在的docker程序id也即父程序id= 6310,可以定位出service-item服務最消耗記憶體資源。定位到服務之後,即可使用docker exec -it service-item ‘/bin/sh’檢視容器內部資訊。

JVM調優基礎命令

在容器內部,就可以進一步使用jdk提供的jps、jstack、jstat、jmap等工具來進行jvm問題排查和調優。

jps[options] [hostid]

jps主要用來輸出JVM中執行的程序狀態資訊。

  • -q 輸出類名、Jar名和傳入main方法的引數
  • -m 輸出傳入main方法的引數
  • -l 輸出main類或Jar的全限名
  • -v 輸出傳入JVM的引數

如下檢視執行的java程序資訊,列印jar名以及執行main方法傳入的引數:

/opt/app # jps -l -m
6 /opt/app/app.jar --server.port=8080
327 sun.tools.jps.Jps -l -m

jstat

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>] 

jstat命令可以用於持續觀察虛擬機器記憶體中各個分割槽的使用率以及GC的統計資料。vmid是Java虛擬機器ID,在Linux/Unix系統取程序ID。
如下面輸出的資訊,取樣時間間隔為1000ms,取樣5次:

/opt/app #  jstat -gc 6 1000 5
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT   
1536.0 1536.0 1233.7  0.0   171520.0 169769.2  249344.0   57018.6   93912.0 91906.8 11264.0 10853.7   6224   47.439   5      3.423   50.863
1536.0 1536.0 1233.7  0.0   171520.0 169805.4  249344.0   57018.6   93912.0 91906.8 11264.0 10853.7   6224   47.439   5      3.423   50.863
1536.0 1536.0  0.0   1536.0 171520.0  3527.9   249344.0   60347.4   96728.0 94808.1 11520.0 11174.7   6225   47.453   5      3.423   50.876
1536.0 1536.0  0.0   1536.0 171520.0  4742.1   249344.0   60347.4   96728.0 94808.1 11520.0 11174.7   6225   47.453   5      3.423   50.876
1536.0 1536.0  0.0   1536.0 171520.0  7589.3   249344.0   60347.4   96728.0 94808.1 11520.0 11174.7   6225   47.453   5      3.423   50.876

上述各個列的含義:

  • S0C、S1C、S0U、S1U:young代的Survivor 0/1區容量(Capacity)和使用量(Used)。0是FromSurvivor,1是ToSurvivor。
  • EC、EU:Eden區容量和使用量
  • OC、OU:年老代容量和使用量
  • MC、MU:元資料區(Metaspace)已經committed的記憶體空間和使用量
  • CCSC、CCSU:壓縮Class(Compressed class space)committed的記憶體空間和使用量。
  • YGC、YGT:young代GC次數和GC耗時
  • FGC、FGCT:Full GC次數和Full GC耗時
  • GCT:GC總耗時

可以通過分割槽佔用量上看到,在第2-3秒之間發生了一次YGC。YGC次數+1,並且Survivor from區的記憶體空間從1233.7->0,Survivor from從0->1536。Eden區也釋放了很多記憶體空間。其他變化的空間佔用也有元資料區以及元資料區的壓縮Class區。Compressed class space也是元資料區的一部分,預設是1G,也可以關閉。具體的jvm8記憶體分佈不再詳述。下一篇GC優化會再展開整理下。
如果只看gc的總統計資訊,也可以用jstat -gcutil vmid查詢:

/opt/app # jstat -gcutil 6 
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00 100.00  73.76  24.20  98.02  97.00   6225   47.453     5    3.423   50.876  

jmap [option] pid

jmap可以用來檢視堆記憶體的使用詳情。記憶體各個分割槽可以通過jmap -heap pid來檢視。得到的輸出如下:

$jmap -heap 6
Attaching to process ID 6, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.121-b13

using thread-local object allocation.
Parallel GC with 2 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 0
   MaxHeapFreeRatio         = 100
   MaxHeapSize              = 536870912 (512.0MB)
   NewSize                  = 44564480 (42.5MB)
   MaxNewSize               = 178782208 (170.5MB)
   OldSize                  = 89653248 (85.5MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 0 (0.0MB)

Heap Usage:
PS Young Generation
Eden Space:
   capacity = 170393600 (162.5MB)
   used     = 99020080 (94.43290710449219MB)
   free     = 71373520 (68.06709289550781MB)
   58.11255821814904% used
From Space:
   capacity = 4194304 (4.0MB)
   used     = 786432 (0.75MB)
   free     = 3407872 (3.25MB)
   18.75% used
To Space:
   capacity = 4194304 (4.0MB)
   used     = 0 (0.0MB)
   free     = 4194304 (4.0MB)
   0.0% used
PS Old Generation
   capacity = 255328256 (243.5MB)
   used     = 65264912 (62.24147033691406MB)
   free     = 190063344 (181.25852966308594MB)
   25.561178783126927% used

39531 interned Strings occupying 4599760 bytes.

Heap Configuration是堆記憶體的配置資訊。可以通過執行引數改變。一般通過分析記憶體分佈和使用情況以及GC資訊,可以針對不同的應用不斷調整到合適的堆記憶體分割槽配置。
Heap Usage可以看堆記憶體實時的佔用情況。

使用jmap -histo[:live] pid檢視堆記憶體中的物件的數目,佔用記憶體(單位是byte),如果帶上live則只統計活物件,如下:

/opt/app/logs # jmap -histo:live 6 | more

 num     #instances         #bytes  class name
----------------------------------------------
   1:        127610       19132008  [C
   2:          6460        4074512  [B
   3:         37041        3259608  java.lang.reflect.Method
   4:        125182        3004368  java.lang.String
   5:         86616        2771712  java.util.concurrent.ConcurrentHashMap$Node
   6:         70783        2265056  java.util.HashMap$Node
   7:         17686        1967496  java.lang.Class
   8:         15834        1448440  [Ljava.util.HashMap$Node;
   9:         35360        1414400  java.util.LinkedHashMap$Entry
  10:         21948        1231624  [Ljava.lang.Object;
  11:          9940        1165728  [I
  12:           986        1064480  [Ljava.util.concurrent.ConcurrentHashMap$Node;
  13:         18685        1046360  java.util.LinkedHashMap
  14:         30351         971232  java.lang.ref.WeakReference
  15:         50340         805440  java.lang.Object
  16:         13490         539600  java.lang.ref.SoftReference
  17:         17705         513768  [Ljava.lang.String;
  18:         18781         450744  org.springframework.security.access.method.DelegatingMethodSecurityMetadataSource$DefaultCacheKey
  19:         20272         434456  [Ljava.lang.Class;
  20:         17270         414480  java.beans.MethodRef
  21:         23616         377856  java.lang.Integer
  22:         11192         358144  java.util.LinkedList
  23:         14911         357864  java.util.ArrayList
  24:          5700         319200  java.beans.MethodDescriptor


以上示例的排序是按照佔用記憶體位元組數倒序的。class name列中”[C,[B,[I “是代表char,byte,int.”[L+類名”代表其他例項。這種寫法跟Class檔案的Java的型別表述含義是一致的。

在進行問題排查時,可以使用jmap把程序記憶體使用情況dump到檔案中,或者dump**.hprof**檔案,在本地使用MAT(Eclipse Memory Analyzer)進行分析。也可以直接用jhat分析檢視。

/opt/app# jmap -dump:format=b,file=heapdump 6
Dumping heap to /opt/app/logs/heapdump ...
Heap dump file created        


/opt/app# jmap -dump:live,format=b,file=heapLive.hprof 6  

jstack [option] pid

jstack可以用來檢視Java程序內的執行緒堆疊資訊。

-l long listings,會打印出額外的鎖資訊,在發生死鎖時可以用jstack -l pid來觀察鎖持有情況
-m mixed mode,不僅會輸出Java堆疊資訊,還會輸出C/C++堆疊資訊(比如Native方法)

輸出資訊如下:

/opt/app # jstack -l 6
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.121-b13 mixed mode):

"elasticsearch[Oneg the Prober][listener][T#1]" #221 daemon prio=5 os_prio=0 tid=0x00007fc2a418a800 nid=0x195 waiting on condition [0x00007fc28318d000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    - parking to wait for  <0x00000000e29f88d0> (a java.util.concurrent.LinkedTransferQueue)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
    at java.util.concurrent.LinkedTransferQueue.awaitMatch(LinkedTransferQueue.java:737)
    at java.util.concurrent.LinkedTransferQueue.xfer(LinkedTransferQueue.java:647)
    at java.util.concurrent.LinkedTransferQueue.take(LinkedTransferQueue.java:1269)
    at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)

   Locked ownable synchronizers:
    - None

在線上問題排查執行緒鎖資訊時,jstack是個非常好用的工具,結合應用日誌可以迅速定位到問題執行緒。

Java效能分析工具

對於Java效能調優,以前一直比較好用的工具是JRockit,JProfile(商業)等工具,但隨著JDK7 up40版本之後,jdk會自帶JMC(JavaMissionControl)工具。可以分析本地應用以及連線遠端ip使用。提供了實時分析執行緒、記憶體,CPU、GC等資訊的視覺化介面。從jdk8 up40開始,JMC還提供了在執行時建立JFR記錄(飛行記錄器)。如果是全面分析heap dump,再綜合使用MAT(Eclipse Memory Analyzer)。基本就可以做很多日常的效能調優以及線上問題排查了。下文簡單介紹一些JMC,基於java version “1.8.0_60”。

Java Mission Control

在Mac上使用的話,需要先找到jdk中的jmc路徑。

$find /Library/Java -name missioncontrol   

在我本地的目錄是/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/lib/missioncontrol/。開啟jmc之後,常用的話可以留在dock下面。
啟動需要觀察的應用,然後即可在JMC的MBean伺服器中觀察到綜合資訊如下:
Java Mission Control 概覽

進一步觀察記憶體以及GC的情況,檢視如下,可以觀察到執行時記憶體各個分割槽的佔用率。對於“堆直方圖”預設是不開啟的,可以通過右上角的重新整理值來啟用,會影響效能。一般用於排查記憶體中的大物件的回收問題以及OOM問題時可以開啟觀察。

Java Mission Control 記憶體分析

對於執行緒死鎖、執行緒池資源方面的分析,可以到執行緒檢視中觀察活動執行緒。

Java Mission Control 執行緒分析

Java飛行記錄器(Java Flight Recorder)

Java 8 up40開始,可以使用JMC建立JFR記錄。JFR可以取樣分析收集Java應用程式以及JVM的資訊, 它的最小開銷小於2%,不會影響其他JVM優化。JFR不會記錄所有方法呼叫,只會探測熱點方法,但不包含Native方法的執行緒取樣。如果要開啟JFR,需要應用啟動引數中新增:

 -XX:+UnlockCommertialFeatures -XX:+FlightRecorder   

一般還是建議本地調優和分析時使用。JFR可以提供固定時間的取樣(預設是1min),以及持續時間的記錄。它們都會dump到一個“.jfr”的檔案中。
分析記憶體資訊如下,可以看到記憶體使用量,以及基礎的GC配置和統計資訊:
JMC-記憶體資訊概覽
詳細分析記憶體情況時,需要進一步檢視“記憶體分配”以及“物件統計資訊”。其中“物件統計資訊”也是預設不開啟的,需要在建立jfr時選擇“啟用”如下:
JFR-Object count
然後即可看到物件統計資訊:
JFR-object count統計

對於熱點方法以及熱點執行緒的取樣分析圖表也很直觀,在分析一些迴圈呼叫時可以重點關注熱點方法,對於有問題的熱點方法可以進一步檢視“堆疊跟蹤”下的呼叫鏈:
JFR-熱點方法

JFR-熱點執行緒

總結

本文主要介紹了java常用的效能優化和排查問題的工具,以及JavaMissionControl工具的一些功能。JMC是官方提供的免費工具,結合MAT,基本可以處理效能優化的80%場景。JMC還可以連結遠端ip進行分析。但對於線上問題排查,還是建議使用jstat,jstack,jmap工具等,結合top、vmstat等快速排查和定位問題。
效能排查一般問題都集中在cpu、記憶體。前者分析執行緒,後者分析具體出現問題的記憶體分割槽。對於磁碟、IO等資源瓶頸需要綜合很多業務場景進行具體定位。
還有一部分比較大的話題,就是GC,會在下一篇進行總結和整理。