JVM調優工具錦囊

Arthas線上

分析診斷調優工具

以前我們要排查線上問題,通常使用的是jdk自帶的調優工具和命令。最常見的就是dump線上日誌,然後下載到本地,導入到jvisualvm工具中。這樣操作有諸多不變,現在阿里團隊開發的Arhtas工具,擁有非常強大的功能,並且都是線上的剛需,尤其是情況緊急,不方便立刻發版,適合臨時處理危急情況使用。下面分兩部分來研究JVM性能調優工具:

1.JDK自帶的性能調優工具

雖然有了Arthas,但也不要忘記JDK自帶的性能調優工具,在某些場景下,他還是有很大作用的。而且Arthas裏面很多功能其根本就是封裝了JDK自帶的這些調優命令。

2.Arthas線上分析工具的使用

這一部分,主要介紹幾個排查線上問題常用的方法。功能真的很強大,剛興趣的猿媛可以研究其基本原理。之前跟我同事討論,感覺這就像病毒一樣,可以修改內存里的東西,真的還是挺強大的。

以上兩種方式排查線上問題,沒有優劣之分,如果線上不能安裝Arthas就是jdk自帶命令,如果jdk自帶命令不能滿足部分要求,又可以安裝Arthas,那就使用Arthas。他們只是排查問題的工具,重要的是排查問題的思路。不管黑貓、白貓,能抓住耗子就是好貓。

一、JDK自帶的調優工具

這裡不是流水一樣的介紹功能怎麼用,就說說線上遇到的問題,我們通常怎麼排查,排查的幾種情況。

  • 內存溢出,出現OutOfMemoryError,這個問題如何排查
  • CPU使用猛增,這個問題如何排查?
  • 進程有死鎖,這個問題如何排查?
  • JVM參數調優

下面來一個一個解決

1、處理內存溢出,報OutOfMemoryError錯誤

第一步:通過jmap -histo命令查看系統內存使用情況

使用的命令:

jmap -histo 進程號

運行結果:

num     #instances         #bytes  class name
----------------------------------------------
	 1:       1101980      372161752  [B
   2:        551394      186807240  [Ljava.lang.Object;
   3:       1235341      181685128  [C
   4:         76692      170306096  [I
   5:        459168       14693376  java.util.concurrent.locks.AbstractQueuedSynchronizer$Node
   6:        543699       13048776  java.lang.String
   7:        497636       11943264  java.util.ArrayList
   8:        124271       10935848  java.lang.reflect.Method
   9:        348582        7057632  [Ljava.lang.Class;
  10:        186244        5959808  java.util.concurrent.ConcurrentHashMap$Node
  8671:      1             16  zipkin2.reporter.Reporter$1
  8672:      1             16  zipkin2.reporter.Reporter$2

Total       8601492      923719424                                   
                                     
                                     
num:序號
instances:實例數量
bytes:佔用空間大小
class name:類名稱,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int[][]

通過這個命令,我們可以看出當前哪個對象最消耗內存。

上面這個運行結果是我啟動了本地的一個項目,然後運行【jmap -histro 進程號】運行出來的結果,直接去了其中的一部分。通過這裡我們可以看看大的實例對象中,有沒有我們自定義的實例對象。通過這個可以排查出哪個實例對象引起的內存溢出。

除此之外,Total匯總數據可以看出當前一共有多少個對象,暫用了多大內存空間。這裡是有約860w個對象,佔用約923M的空間。

第二步:分析內存溢出,查看堆空間佔用情況

使用命令

jhsdb jmap --heap --pid 進程號

比如,我本地啟動了一個項目,想要查看這個項目的內存佔用情況:

[root@iZ2pl8Z ~]# jhsdb jmap --heap --pid 28692
  
Attaching to process ID 28692, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.13+10-LTS-370

using thread-local object allocation.
Garbage-First (G1) GC with 4 thread(s)

Heap Configuration:
   MinHeapFreeRatio         = 40
   MaxHeapFreeRatio         = 70
   MaxHeapSize              = 2065694720 (1970.0MB)
   NewSize                  = 1363144 (1.2999954223632812MB)
   MaxNewSize               = 1239416832 (1182.0MB)
   OldSize                  = 5452592 (5.1999969482421875MB)
   NewRatio                 = 2
   SurvivorRatio            = 8
   MetaspaceSize            = 21807104 (20.796875MB)
   CompressedClassSpaceSize = 1073741824 (1024.0MB)
   MaxMetaspaceSize         = 17592186044415 MB
   G1HeapRegionSize         = 1048576 (1.0MB)

Heap Usage:
G1 Heap:
   regions  = 1970
   capacity = 2065694720 (1970.0MB)
   used     = 467303384 (445.65523529052734MB)
   free     = 1598391336 (1524.3447647094727MB)
   22.622093161955704% used
G1 Young Generation:
Eden Space:
   regions  = 263
   capacity = 464519168 (443.0MB)
   used     = 275775488 (263.0MB)
   free     = 188743680 (180.0MB)
   59.36794582392776% used
Survivor Space:
   regions  = 6
   capacity = 6291456 (6.0MB)
   used     = 6291456 (6.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:
   regions  = 179
   capacity = 275775488 (263.0MB)
   used     = 186285016 (177.65523529052734MB)
   free     = 89490472 (85.34476470947266MB)
   67.54951912187352% used

下面來看看參數的含義

堆空間配置信息

Heap Configuration:
  /**
   * 空閑堆空間的最小百分比,計算公式為:HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的區間為0	  * 到100,默認值為 40。如果HeapFreeRatio < MinHeapFreeRatio,則需要進行堆擴容,擴容的時機應該在每次垃圾回收之後。
   */
	MinHeapFreeRatio = 40  
  /**
   * 空閑堆空間的最大百分比,計算公式為:HeapFreeRatio =(CurrentFreeHeapSize/CurrentTotalHeapSize) * 100,值的區間為0  
   * 到100,默認值為 70。如果HeapFreeRatio > MaxHeapFreeRatio,則需要進行堆縮容,縮容的時機應該在每次垃圾回收之後
   */
  MaxHeapFreeRatio         = 70
  /**JVM 堆空間允許的最大值*/
  MaxHeapSize              = 2065694720 (1970.0MB)
  /** JVM 新生代堆空間的默認值*/
  NewSize                  = 1363144 (1.2999954223632812MB)
  /** JVM 新生代堆空間允許的最大值 */
  MaxNewSize               = 1239416832 (1182.0MB)
  /** JVM 老年代堆空間的默認值 */
  OldSize                  = 5452592 (5.1999969482421875MB)
  /** 新生代(2個Survivor區和Eden區 )與老年代(不包括永久區)的堆空間比值,表示新生代:老年代=1:2*/
  NewRatio                 = 2
  /** 兩個Survivor區和Eden區的堆空間比值為 8,表示 S0 : S1 :Eden = 1:1:8 */
  SurvivorRatio            = 8
  /** JVM 元空間的默認值 */
  MetaspaceSize            = 21807104 (20.796875MB)
  CompressedClassSpaceSize = 1073741824 (1024.0MB)
  /** JVM 元空間允許的最大值 */
  MaxMetaspaceSize         = 17592186044415 MB
  /** 在使用 G1 垃圾回收算法時,JVM 會將 Heap 空間分隔為若干個 Region,該參數用來指定每個 Region 空間的大小 */
  G1HeapRegionSize         = 1048576 (1.0MB) 

G1堆使用情況

Heap Usage:
G1 Heap:
   regions  = 1970
   capacity = 2065694720 (1970.0MB)
   used     = 467303384 (445.65523529052734MB)
   free     = 1598391336 (1524.3447647094727MB)
   22.622093161955704% used
     
G1 的 Heap 使用情況,該 Heap 包含 1970 個 Region,結合上文每個 RegionSize=1M,因此 Capacity = Regions * RegionSize = 1970 * 1M = 1970M,已使用空間為 445.65M,空閑空間為 1524.34M,使用率為 22.62%。

G1年輕代Eden區使用情況

G1 Young Generation:
Eden Space:
   regions  = 263
   capacity = 464519168 (443.0MB)
   used     = 275775488 (263.0MB)
   free     = 188743680 (180.0MB)
   59.36794582392776% used

     
     
G1 的 Eden 區的使用情況,總共使用了 263 個 Region,結合上文每個 RegionSize=1M,因此 Used = Regions * RegionSize = 263 * 1M = 263M,Capacity=443M 表明當前 Eden 空間分配了 443 個 Region,使用率為 59.37%。

G1年輕代Survivor區使用情況和G1老年代使用情況:和Eden區類似

Survivor Space:
   regions  = 6
   capacity = 6291456 (6.0MB)
   used     = 6291456 (6.0MB)
   free     = 0 (0.0MB)
   100.0% used
     
G1 Old Generation:
   regions  = 179
   capacity = 275775488 (263.0MB)
   used     = 186285016 (177.65523529052734MB)
   free     = 89490472 (85.34476470947266MB)
   67.54951912187352% used
     
Survivor區使用情況和Eden區類似。 老年代參數含義和Eden區類似。

通過上面的命令,我們就能知道當前系統堆空間的使用情況了,到底是老年代有問題還是新生代有問題。

第三步:導出dump內存溢出的文件,導入到jvisualvm查看

如果前兩種方式還是沒有排查出問題,我們可以導出內存溢出的日誌,在導入客戶端進行分析

使用的命令是:

jmap -dump:file=a.dump 進程號

或者是直接設置JVM參數

-XX:+HeapDumpOnOutOfMemoryError 
-XX:HeapDumpPath=./ (路徑)

然後導入到jvisualvm中進行分析,方法是:點擊文件->裝入,導入文件,查看系統的運行情況了。

image

通過分析實例數,看看哪個對象實例佔比最高,這裡重點看我們自定義的類,然後分析這個對象裏面有沒有大對象,從而找出引起內存溢出的根本原因。

2、CPU使用猛增,這個問題如何排查?

我們可以通過Jstack找出佔用cpu最高的線程的堆棧信息,下面來一步一步分析。

假設我們有一段死循環,不斷執行方法調用,線程始終運行不釋放就會導致CPU飆高,示例代碼如下:

package com.lxl.jvm;

public class Math {
    public static int initData = 666;
    public static User user = new User();
    public User user1;

    public int compute() {
        int a = 1;
        int b = 2;
        int c = (a + b) * 10;
        return c;
    }

    public static void main(String[] args) {
        Math math = new Math();
        while(true){
            math.compute();

        }
    }
}

第一步:運行代碼,使用top命令查看cpu佔用情況

image

如上,現在有一個java進程,cpu嚴重飆高了,接下來如何處理呢?

第二步:使用top -p 命令查看飆高進程

top -p 46518

image

我們看到了單獨的46518這個線程的詳細信息

第三步:按H,獲取每個線程的內存情況

需要注意的是,這裡的H是大寫的H。

img

我們可以看出線程0和線程1線程號飆高。

第四步:找到內存和cpu佔用最高的線程tid

通過上圖我們看到佔用cpu資源最高的線程有兩個,線程號分別是4018362,4018363。我們一第一個為例說明,如何查詢這個線程是哪個線程,以及這個線程的什麼地方出現問題,導致cpu飆高。

第五步:將線程tid轉化為十六進制

67187778是線程號為4013442的十六進制數。具體轉換可以網上查詢工具。

第六步:執行[ jstack 4018360|grep -A 10 67187778] 查詢飆高線程的堆棧信息

接下來查詢飆高線程的堆棧信息

 jstack 4013440|grep -A 10 67190882
  • 4013440:表示的是進程號
  • 67187778: 表示的是線程號對應的十六進制數

通過這個方式可以查詢到這個線程對應的堆棧信息

img

從這裡我們可以看出有問題的線程id是0x4cd0, 哪一句代碼有問題呢,Math類的22行。

第七步:查看對應的堆棧信息找出可能存在問題的代碼

上述方法定位問題已經很精確了,接下來就是區代碼里排查為什麼會有問題了。

備註:上面的進程id可能沒有對應上,在測試的時候,需要寫對進程id和線程id

3、進程有死鎖,這個問題如何排查?

Jstack可以用來查看堆棧使用情況,以及進程死鎖情況。下面就來看看如何排查進程死鎖

還是通過案例來分析

package com.lxl.jvm;

public class DeadLockTest {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lock1) {
                try {
                    System.out.println("thread1 begin");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {

                }
                synchronized (lock2) {
                    System.out.println("thread1 end");
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (lock2) {
                try {
                    System.out.println("thread2 begin");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {

                }
                synchronized (lock1) {
                    System.out.println("thread2 end");
                }
            }
        }).start();
    }
}

上面是兩把鎖,互相調用。

  1. 定義了兩個成員變量lock1,lock2
  2. main方法中定義了兩個線程。
    • 線程1內部使用的是同步執行–上鎖,鎖是lock1。休眠5秒鐘之後,他要獲取第二把鎖,執行第二段代碼。
    • 線程2和線程1類似,鎖相反。
  3. 問題:一開始,像個線程並行執行,線程一獲取lock1,線程2獲取lock2.然後線程1繼續執行,當休眠5s後獲取開啟第二個同步執行,鎖是lock2,但這時候很可能線程2還沒有執行完,所以還沒有釋放lock2,於是等待。線程2剛開始獲取了lock2鎖,休眠五秒後要去獲取lock1鎖,這時lock1鎖還沒釋放,於是等待。兩個線程就處於相互等待中,造成死鎖。

第一步:通過Jstack命令來看看是否能檢測到當前有死鎖。

jstack 51789

img

從這裏面個異常可以看出,

  • prio:當前線程的優先級
  • cpu:cpu耗時
  • os_prio:操作系統級別的優先級
  • tid:線程id
  • nid:系統內核的id
  • state:當前的狀態,BLOCKED,表示阻塞。通常正常的狀態是Running我們看到Thread-0和Thread-1線程的狀態都是BLOCKED.

通過上面的信息,我們判斷出兩個線程的狀態都是BLOCKED,可能有點問題,然後繼續往下看。

img

我們從最後的一段可以看到這句話:Found one Java-level deadlock; 意思是找到一個死鎖。死鎖的線程號是Thread-0,Thread-1。

Thread-0:正在等待0x000000070e706ef8對象的鎖,這個對象現在被Thread-1持有。

Thread-1:正在等待0x000000070e705c98對象的鎖,這個對象現在正在被Thread-0持有。

最下面展示的是死鎖的堆棧信息。死鎖可能發生在DeadLockTest的第17行和第31行。通過這個提示,我們就可以找出死鎖在哪裡了。

第二步:使用jvisualvm查看死鎖

如果使用jstack感覺不太方便,還可以使用jvisualvm,通過界面來查看,更加直觀。

在程序代碼啟動的過程中,打開jvisualvm工具。

img

找到當前運行的類,查看線程,就會看到最頭上的一排紅字:檢測到死鎖。然後點擊「線程Dump」按鈕,查看相信的線程死鎖的信息。

img

這裡可以找到線程私鎖的詳細信息,具體內容和上面使用Jstack命令查詢的結果一樣,這裡實用工具更加方便。

4、JVM參數調優

jvm調優通常使用的是Jstat命令。

1. 垃圾回收統計 jstat -gc

jstat -gc 進程id

這個命令非常常用,在線上有問題的時候,可以通過這個命令來分析問題。

下面我們來測試一下,啟動一個項目,然後在終端駛入jstat -gc 進程id,得到如下結果:

img

上面的參數分別是什麼意思呢?先識別參數的含義,然後根據參數進行分析

  • S0C: 第一個Survivor區的容量
  • S1C: 第二個Survivor區的容量
  • S0U: 第一個Survivor區已經使用的容量
  • S1U:第二個Survivor區已經使用的容量
  • EC: 新生代Eden區的容量
  • EU: 新生代Eden區已經使用的容量
  • OC: 老年代容量
  • OU:老年代已經使用的容量
  • MC: 方法區大小(元空間)
  • MU: 方法區已經使用的大小
  • CCSC:壓縮指針佔用空間
  • CCSU:壓縮指針已經使用的空間
  • YGC: YoungGC已經發生的次數
  • YGCT: 這一次YoungGC耗時
  • FGC: Full GC發生的次數
  • FGCT: Full GC耗時
  • GCT: 總的GC耗時,等於YGCT+FGCT

連續觀察GC變化的命令

jstat -gc 進程ID 間隔時間  打印次數

舉個例子:我要打印10次gc信息,每次間隔1秒

jstat -gc 進程ID 1000 10

image

這樣就連續打印了10次gc的變化,每次隔一秒。

這個命令是對整體垃圾回收情況的統計,下面將會差分處理。

2.堆內存統計

這個命令是打印堆內存的使用情況。

jstat -gccapacity 進程ID

a

  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:當前新生代容量
  • S0C:第一個Survivor區大小
  • S1C:第二個Survivor區大小
  • EC:Eden區的大小
  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:當前老年代大小
  • OC: 當前老年代大小
  • MCMN: 最小元數據容量
  • MCMX:最大元數據容量
  • MC:當前元數據空間大小
  • CCSMN:最小壓縮類空間大小
  • CCSMX:最大壓縮類空間大小
  • CCSC:當前壓縮類空間大小
  • YGC:年輕代gc次數
  • FGC:老年代GC次數

3.新生代垃圾回收統計

命令:

jstat -gcnew 進程ID [ 間隔時間  打印次數]

這個指的是當前某一次GC的內存情況

img

  • S0C:第一個Survivor的大小
  • S1C:第二個Survivor的大小
  • S0U:第一個Survivor已使用大小
  • S1U:第二個Survivor已使用大小
  • TT: 對象在新生代存活的次數
  • MTT: 對象在新生代存活的最大次數
  • DSS: 期望的Survivor大小
  • EC:Eden區的大小
  • EU:Eden區的使用大小
  • YGC:年輕代垃圾回收次數
  • YGCT:年輕代垃圾回收消耗時間

4. 新生代內存統計

jstat -gcnewcapacity 進程ID

image

參數含義:

  • NGCMN:新生代最小容量
  • NGCMX:新生代最大容量
  • NGC:當前新生代容量
  • S0CMX:Survivor 1區最大大小
  • S0C:當前Survivor 1區大小
  • S1CMX:Survivor 2區最大大小
  • S1C:當前Survivor 2區大小
  • ECMX:最大Eden區大小
  • EC:當前Eden區大小
  • YGC:年輕代垃圾回收次數
  • FGC:老年代回收次數

5. 老年代垃圾回收統計

命令:

jstat -gcold 進程ID

image

參數含義:

  • MC:方法區大小
  • MU:方法區已使用大小
  • CCSC:壓縮指針類空間大小
  • CCSU:壓縮類空間已使用大小
  • OC:老年代大小
  • OU:老年代已使用大小
  • YGC:年輕代垃圾回收次數
  • FGC:老年代垃圾回收次數
  • FGCT:老年代垃圾回收消耗時間
  • GCT:垃圾回收消耗總時間,新生代+老年代

6. 老年代內存統計

命令:

jstat -gcoldcapacity 進程ID

image

參數含義:

  • OGCMN:老年代最小容量
  • OGCMX:老年代最大容量
  • OGC:當前老年代大小
  • OC:老年代大小
  • YGC:年輕代垃圾回收次數
  • FGC:老年代垃圾回收次數
  • FGCT:老年代垃圾回收消耗時間
  • GCT:垃圾回收消耗總時間

7. 元數據空間統計

命令

jstat -gcmetacapacity 進程ID

image

  • MCMN:最小元數據容量
  • MCMX:最大元數據容量
  • MC:當前元數據空間大小
  • CCSMN:最小指針壓縮類空間大小
  • CCSMX:最大指針壓縮類空間大小
  • CCSC:當前指針壓縮類空間大小
  • YGC:年輕代垃圾回收次數
  • FGC:老年代垃圾回收次數
  • FGCT:老年代垃圾回收消耗時間
  • GCT:垃圾回收消耗總時間

8.整體運行情況

命令:

jstat -gcutil 進程ID

image

  • S0:Survivor 1區當前使用比例
  • S1:Survivor 2區當前使用比例
  • E:Eden區使用比例
  • O:老年代使用比例
  • M:元數據區使用比例
  • CCS:指針壓縮使用比例
  • YGC:年輕代垃圾回收次數
  • YGCT:年輕代垃圾回收消耗時間
  • FGC:老年代垃圾回收次數
  • FGCT:老年代垃圾回收消耗時間
  • GCT:垃圾回收消耗總時間

通過查詢上面的參數來分析整個堆空間。

二、Arthas線上分析工具的使用

Arthas的功能非常強大,現附上官方文檔://arthas.aliyun.com/doc/

其實想要了解Arthas,看官方文檔就可以了,功能全而詳細。那為什麼還要整理一下呢?我們這裡整理的是一些常用功能,以及在緊急情況下可以給我們幫大忙的功能。

Arthas分為幾個部分來研究,先來看看我們的研究思路哈

1.安裝及啟動—這一塊簡單看,對於程序員來說,so easy

2.dashboard儀錶盤功能—類似於JDK的jstat命令,

3.thread命令查詢進行信息—類似於jmap命令

4.反編譯線上代碼—-這個功能很牛,改完發版了,怎麼沒生效,反編譯看看。

5.查詢某一個函數的返回值

6.查詢jvm信息,並修改—-當發生內存溢出是,可以手動設置打印堆日誌到文件

7.profiler火焰圖

下面就來看看Arthas的常用功能的用法吧

1、Arthas的安裝及啟動

其實說到這快,不得不提的是,之前我一直因為arthas是一個軟件,要啟動,界面操作。當時我就想,要是這樣,在線上安裝一個單獨的應用,公司肯定不同意啊~~~,研究完才發現,原來Arthas就是一個jar包。運行起來就是用java -jar 就可以。

1) 安裝

可以直接在Linux上通過命令下載:

wget //alibaba.github.io/arthas/arthas-boot.jar

也可以在瀏覽器直接訪問//alibaba.github.io/arthas/arthas-boot.jar,等待下載成功後,上傳到Linux服務器上。

2) 啟動

執行命令就可以啟動了

java -jar arthas-boot.jar

啟動成功可以看到如下界面:

image

然後找到你想監控的進程,輸入前面對應的編號,就可以開啟進行監控模式了。比如我要看4

image

看到這個就表示,進入應用監聽成功

2、dashboard儀錶盤–查詢整體項目運行情況

執行命令

dashboard

image

這裏面一共有三塊

1)線程信息

我們可以看到當前進程下所有的線程信息。其中第13,14號線程當前處於BLOCKED阻塞狀態,阻塞時間也可以看到。通過這個一目了然,當前有兩個線程是有問題的,處於阻塞狀態GC線程有6個。

2)內存信息

內存信息包含三個部分:堆空間信息、非堆空間信息和GC垃圾收集信息

堆空間信息

  • g1_eden_space: Eden區空間使用情況
  • g1_survivor_space: Survivor區空間使用情況
  • g1_old_gen: Old老年代空間使用情況

非堆空間信息

  • codeheap_’non-nmethods’: 非方法代碼堆大小
  • metaspace: 元數據空間使用情況
  • codeheap_’profiled_nmethods’:
  • compressed_class_space: 壓縮類空間使用情況

GC垃圾收集信息

  • gc.g1_young_generation.count:新生代gc的數量
  • gc.g1_young_generation.time(ms)新生代gc的耗時
  • gc.g1_old_generation.count: 老年代gc的數量
  • gc.g1_old_generation.time(ms):老年代gc的耗時

3) 運行時信息

  • os.name:當前使用的操作系統 Mac OS X

  • os.version :操作系統的版本號 10.16

  • java.version:java版本號 11.0.2

  • java.home:java根目錄 /Library/Java/JavaVirtualMachines/jdk-11.0.2.jdk/Contents/Home

  • systemload.average:系統cpu負載平均值4.43

​ load average值的含義

​ > 單核處理器

​ 假設我們的系統是單CPU單內核的,把它比喻成是一條單向馬路,把CPU任務比作汽車。當車不多的時候,load <1;當車佔滿整個 馬路的時候 load=1;當馬路都站滿了,而且馬路外還堆滿了汽車的時候,load>1

img Load < 1

img Load = 1
img Load >1

​ > 多核處理器

​ 我們經常會發現服務器Load > 1但是運行仍然不錯,那是因為服務器是多核處理器(Multi-core)。

​ 假設我們服務器CPU是2核,那麼將意味我們擁有2條馬路,我們的Load = 2時,所有馬路都跑滿車輛。

imgLoad = 2時馬路都跑滿了

  • processors : 處理器個數 8

  • timestamp/uptime:採集的時間戳Fri Jan 07 11:36:12 CST 2022/2349s

通過儀錶盤,我們能從整體了解當前線程的運行健康狀況

3.thread命令查詢CPU使用率最高的線程及問題原因

通過dashboard我們可以看到當前進程下運行的所有的線程。那麼如果想要具體查看某一個線程的運行情況,可以使用thread命令

1. 統計cpu使用率最高的n個線程

先來看看常用的參數。

參數說明

參數名稱 參數說明
id 線程id
[n:] 指定最忙的前N個線程並打印堆棧
[b] 找出當前阻塞其他線程的線程
[i <value>] 指定cpu使用率統計的採樣間隔,單位為毫秒,默認值為200
[–all] 顯示所有匹配的線程

我們的目標是想要找出CPU使用率最高的n個線程。那麼需要先明確,如何計算出CPU使用率,然後才能找到最高的。計算規則如下:

首先、第一次採樣,獲取所有線程的CPU時間(調用的是java.lang.management.ThreadMXBean#getThreadCpuTime()及sun.management.HotspotThreadMBean.getInternalThreadCpuTimes()接口)
  
然後、睡眠等待一個間隔時間(默認為200ms,可以通過-i指定間隔時間)
  
再次、第二次採樣,獲取所有線程的CPU時間,對比兩次採樣數據,計算出每個線程的增量CPU時間
  線程CPU使用率 = 線程增量CPU時間 / 採樣間隔時間 * 100%
  
  
注意: 這個統計也會產生一定的開銷(JDK這個接口本身開銷比較大),因此會看到as的線程佔用一定的百分比,為了降低統計自身的開銷帶來的影響,可以把採樣間隔拉長一些,比如5000毫秒。

統計1秒內cpu使用率最高的n個線程:

thread -n 3 -i 1000

image

從線程的詳情可以分析出,目前第一個線程的使用率是最高的,cpu佔用了達到99.38%。第二行告訴我們,是Arthas.java這個類的第38行導致的。

由此,我們可以一眼看出問題,然後定位問題代碼的位置,接下來就是人工排查問題了。

2、查詢出當前被阻塞的線程

命令:

thread -b

image

可以看到內容提示,線程Thread-1被線程Thread-0阻塞。對應的代碼行數是DeadLockTest.java類的第31行。根據這個提示去查找代碼問題。

3、指定採樣的時間間隔

命令

thread -i 1000

這個的含義是個1s統計一次採樣

4.反編譯線上代碼—-這個功能很牛,改完發版了,怎麼沒生效,反編譯看看。

說道Arthas,不得不提的一個功能就是線上反編譯代碼的功能。經常會發生的一種狀況是,線上有問題,定位問題後立刻改代碼,可是發版後發現沒生效,不可能啊~~~剛剛提交成功了呀。於是重新發版,只能靠運氣,不知道為啥沒生效。

反編譯線上代碼可以讓我們一目了然知道代碼帶動部分是否生效。反編譯代碼使用Arthas的jad命令

jad 命令將JVM中實際運行的class的byte code反編譯成java代碼

用法:

jad com.lxl.jvm.DeadLockTest

運行結果:

image

運行結果分析:這裡包含3個部分

  • ClassLoader:類加載器就是加載當前類的是哪一個類加載器
  • Location: 類在本地保存的位置
  • 源碼:類反編譯位元組碼後的源碼

如果不想想是類加載信息和本地位置,只想要查看類源碼信息,可以增加–source-only參數

jad --source-only 類全名

6. ognl 動態執行線上的代碼

能夠調用線上的代碼,是不是很神奇了。感覺哪段代碼執行有問題,但是又沒有日誌,就可以使用這個方法動態調用目標方法了。

我們下面的案例都是基於這段代碼執行,User類:

public class User {
    private int id;
    private String name;


    public User() {
    }

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

DeadLockTest類:

public class DeadLockTest {
    private static Object lock1 = new Object();
    private static Object lock2 = new Object();
    private static List<String> names = new ArrayList<>();
    private List<String> citys = new ArrayList<>();



    public static String add() {
        names.add("zhangsan");
        names.add("lisi");
        names.add("wangwu");
        names.add("zhaoliu");

        return "123456";
    }
    public List<String> getCitys() {
        DeadLockTest deadLockTest = new DeadLockTest();
        deadLockTest.citys.add("北京");

        return deadLockTest.citys;
    }


    public static List<User> addUsers(Integer id, String name) {
        List<User> users = new ArrayList<>();
        User user = new User(id, name);
        users.add(user);
        return users;
    }


    public static void main(String[] args) {


        new Thread(() -> {
            synchronized (lock1) {
                try {
                    System.out.println("thread1 begin");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {

                }
                synchronized (lock2) {
                    System.out.println("thread1 end");
                }
            }
        }).start();

        new Thread(() -> {
            synchronized (lock2) {
                try {
                    System.out.println("thread2 begin");
                    Thread.sleep(5000);
                } catch (InterruptedException e) {

                }
                synchronized (lock1) {
                    System.out.println("thread2 end");
                }
            }
        }).start();
    }
}

1)獲取靜態函數

> 返回值是字符串

ognl '@全路徑類名@靜態方法名("參數")'

示例1:在DeadLockTest類中有一個add靜態方法,我們來看看通過ognl怎麼執行這個靜態方法。執行命令

ognl '@com.lxl.jvm.DeadLockTest@add()'
  
其中,第一個@後面跟的是類的全名稱;第二個@跟的是屬性或者方法名,如果屬性是一個對象,想要獲取屬性裏面的屬性或者方法,直接打.屬性名/方法名 即可。

運行效果:
image

我們看到了這個對象的返回值是123456

> 返回值是對象

ognl '@全路徑類名@靜態方法名("參數")' -x 2

這裡我們可以嘗試一下替換-x 2 為 -x 1 ;-x 3;

* 案例1:返回對象的地址。不加 -x 或者是-x 1
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")'
或
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' -x 1

返回值

image

* 案例2:返回對象中具體參數的值。加 -x 2
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' -x 2

返回值

image

* 案例3:返回對象中有其他對象
  • 命令:
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' -x 2

執行結果:

image

-x 2 獲取的是對象的值,List返回的是數組信息,數組長度。

  • 命令:
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' -x 3

執行結果:

image

-x 3 打印出對象的值,對象中List列表中的值。

* 案例4:方法A的返回值當做方法B的入參
ognl '#[email protected]@getCitys(), #[email protected]@generatorUser(1,"lisi",#value1), {#value1,#value2}' -x 2

image

> 方法入參是簡單類型的列表

ognl '@com.lxl.jvm.DeadLockTest@returnCitys({"beijing","shanghai","guangdong"})'

image

> 方法入參是一個複雜對象

ognl '#value1=new com.lxl.jvm.User(1,"zhangsan"),#value1.setName("aaa"), #value1.setCitys({"bj", "sh"}), #[email protected]@addUsers(#value1), #value2' -x 3

image

> 方法入參是一個map對象

ognl '#value1=new com.lxl.jvm.User(1,"zhangsan"), #value1.setCitys({"bj", "sh"}), #value2=#{"mum":"zhangnvshi","dad":"wangxiansheng"}, #value1.setFamily(#value2), #value1' -x 2

image

2)獲取靜態字段

ognl '@全路徑類名@靜態屬性名'

示例:在DeadLockTest類中有一個names靜態屬性,下面來看看如何獲取這個靜態屬性。執行命令:

ognl '@com.lxl.jvm.DeadLockTest@names'
  
其中,第一個@後面跟的是類的全名稱;第二個@跟的是屬性或者方法名,如果屬性是一個對象,想要獲取屬性裏面的屬性或者方法,直接打.屬性名/方法名 即可。  

運行效果:

image

第一次執行獲取屬性命令,返回的屬性是一個空集合;然後執行add方法,往names集合中添加了屬性;再次請求names集合,發現有4個屬性返回。

3) 獲取實例對象

ognl '#value1=new com.lxl.jvm.User(1,"zhangsan"),#value1.setName("aaa"), #value1.setCitys({"bj", "sh"}), {#value1}' -x 2

獲取實例對象,使用new關鍵字,執行結果:

image

7. 線上代碼修改

生產環境有時會遇到非常緊急的問題,或突然發現一個bug,這時候不方便重新發版,或者發版未生效,可以使用Arthas臨時修改線上代碼。通過Arthas修改的步驟如下:

1. 從讀取.class文件
2. 編譯成.java文件
3. 修改.java文件
4. 將修改後的.java文件編譯成新的.class文件
5. 將新的.class文件通過classloader加載進JVM內

第一步:讀取.class文件

sc -d *DeadLockTest*

使用sc命令查看JVM已加載的類信息。關於sc命令,查看官方文檔://arthas.aliyun.com/doc/sc.html

  • -d : 表示打印類的詳細信息

image

最後一個參數classLoaderHash,表示在jvm中類加載的hash值,我們要獲得的就是這個值。

第二步:使用jad命令將.class文件反編譯為.java文件才行

jad -c 7c53a9eb --source-only com.lxl.jvm.DeadLockTest > /Users/lxl/Downloads/DeadLockTest.java
  • jad命令是反編譯指定已加載類的源碼
  • -c : 類所屬 ClassLoader 的 hashcode
  • –source-only:默認情況下,反編譯結果里會帶有ClassLoader信息,通過--source-only選項,可以只打印源代碼。
  • com.lxl.jvm.DeadLockTest:目標類的全路徑
  • /Users/lxl/Downloads/DeadLockTest.java:反編譯文件的保存路徑
       /*
        * Decompiled with CFR.
        * 
        * Could not load the following classes:
        *  com.lxl.jvm.User
        */
       package com.lxl.jvm;
       
       import com.lxl.jvm.User;
       import java.util.ArrayList;
       import java.util.List;
       
       public class DeadLockTest {
           private static Object lock1 = new Object();
           private static Object lock2 = new Object();
           private static List<String> names = new ArrayList<String>();
           private List<String> citys = new ArrayList<String>();
       
           public static List<String> getCitys() {
               DeadLockTest deadLockTest = new DeadLockTest();
/*25*/         deadLockTest.citys.add("北京");
/*27*/         return deadLockTest.citys;
           }
       
          ......
       
           public static void main(String[] args) {
              ......
           }
       }

這裡截取了部分代碼。

第三步:修改java文件

					public static List<String> getCitys() {
               System.out.println("-----這裡增加了一句日誌打印-----");
               DeadLockTest deadLockTest = new DeadLockTest();
/*25*/         deadLockTest.citys.add("北京");
/*27*/         return deadLockTest.citys;
           }

第四步:使用mc命令將.java文件編譯成.class文件

mc -c 512ddf17 -d /Users/luoxiaoli/Downloads /Users/luoxiaoli/Downloads/DeadLockTest.java
  • mc: 編譯.java文件生.class文件, 詳細使用方法參考官方文檔//arthas.aliyun.com/doc/mc.html
  • -c:指定classloader的hash值
  • -d:指定輸出目錄
  • 最後一個參數是java文件路徑

這是反編譯後的class位元組碼文件

image

第五步:使用redefine命令,將.class文件重新加載進JVM

redefine -c /Users/***/Downloads/com/lxl/jvm/DeadLockTest.class

image

最後看到redefine success,表示重新加載.class文件進JVM成功了。

注意事項

redefine命令使用之後,再使用jad命令會使位元組碼重置,恢復為未修改之前的樣子。官方關於redefine命令的說明

第六步:檢驗效果

這裡檢測效果,調用接口,執行日誌即可。

8、實時修改生產環境的日誌級別

這個功能也很好用,通常,我們在日誌中打印的日誌級別一般是infor、warn、error級別的,debug日誌一般看不到。那麼出問題的時候,一些日誌,在寫代碼的時候會被記錄在debug日誌中,而此時日誌級別又很高。那麼迫切需要調整日誌級別。

這個功能很好用啊,我們可以將平時不經常打印出來的日誌設置為debug級別。設置線上日誌打印級別為info。當線上有問題的時候,可以將日誌級別動態調整為debug。異常排查完,在修改回info。這對訪問量特別大日誌內容很多的項目比較有效,可以有效節省日誌輸出帶來的開銷。

第一步:使用logger命令查看日誌級別

image

  • 當前應用的日誌級別是info
  • 類加載的hash值是18b4aac2

我們定義一個接口,其源代碼內容如下:

@PostMapping(value = "test")
    public String test() {
        log.debug("這是一條 debug 級別的日誌");
        log.info("這是一條 info 級別的日誌");
        log.error("這是一條 error 級別的日誌");
        log.warn("這是一條 warn 級別的日誌");
        return "完成";
    }

可以調用接口,查看日誌輸出代碼。

image

我們看到,日誌輸出的是info及以下的級別。

第二步:修改logger日誌的級別

logger -c 18b4aac2 --name ROOT --level debug

修改完日誌級別以後,輸出日誌為debug級別。

image

8. 查詢jvm信息,並修改—-當發生內存溢出時,可以手動設置打印堆日誌到文件

通常查詢jvm參數,使用的是Java自帶的工具[jinfo 進程號]。arthas中通過vmoption獲取jvm參數:

image

假設,我們要設置JVM出現OutOfMemoryError的時候,自動dump堆快照

vmoption  HeapDumpOnOutOfMemoryError true

image

這時,如果發生堆內存溢出,會打印日誌到文件

9. 監控函數耗時

trace 待監控方法的全類名  待監控的方法名
trace com.lxl.jvm.DeadLockTest generatorUser

img

  • 通過圈起來的部分可以看到,接口的入口函數time總耗時371ms
  • 其中getDataFromDb函數耗時200ms
  • getDataFromRedis函數耗時100ms
  • getDataFromOuter函數耗時50ms
  • process函數耗時20ms

很明顯,最慢的函數已經找到了,接下里就要去對代碼進行進一步分析,然後再進行優化

先介紹這些主要得工具,其他工具參考文檔//arthas.aliyun.com/doc/advanced-use.html