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中進行分析,方法是:點擊文件->裝入,導入文件,查看系統的運行情況了。
通過分析實例數,看看哪個對象實例占比最高,這里重點看我們自定義的類,然后分析這個對象里面有沒有大對象,從而找出引起內存溢出的根本原因。
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占用情況
如上,現在有一個java進程,cpu嚴重飆高了,接下來如何處理呢?
第二步:使用top -p 命令查看飆高進程
top -p 46518
我們看到了單獨的46518這個線程的詳細信息
第三步:按H,獲取每個線程的內存情況
需要注意的是,這里的H是大寫的H。
我們可以看出線程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: 表示的是線程號對應的十六進制數
通過這個方式可以查詢到這個線程對應的堆棧信息
從這里我們可以看出有問題的線程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();
}
}
上面是兩把鎖,互相調用。
- 定義了兩個成員變量lock1,lock2
- main方法中定義了兩個線程。
- 線程1內部使用的是同步執行--上鎖,鎖是lock1。休眠5秒鍾之后,他要獲取第二把鎖,執行第二段代碼。
- 線程2和線程1類似,鎖相反。
- 問題:一開始,像個線程並行執行,線程一獲取lock1,線程2獲取lock2.然后線程1繼續執行,當休眠5s后獲取開啟第二個同步執行,鎖是lock2,但這時候很可能線程2還沒有執行完,所以還沒有釋放lock2,於是等待。線程2剛開始獲取了lock2鎖,休眠五秒后要去獲取lock1鎖,這時lock1鎖還沒釋放,於是等待。兩個線程就處於相互等待中,造成死鎖。
第一步:通過Jstack命令來看看是否能檢測到當前有死鎖。
jstack 51789
從這里面個異常可以看出,
- prio:當前線程的優先級
- cpu:cpu耗時
- os_prio:操作系統級別的優先級
- tid:線程id
- nid:系統內核的id
- state:當前的狀態,BLOCKED,表示阻塞。通常正常的狀態是Running我們看到Thread-0和Thread-1線程的狀態都是BLOCKED.
通過上面的信息,我們判斷出兩個線程的狀態都是BLOCKED,可能有點問題,然后繼續往下看。
我們從最后的一段可以看到這句話: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工具。
找到當前運行的類,查看線程,就會看到最頭上的一排紅字:檢測到死鎖。然后點擊“線程Dump”按鈕,查看相信的線程死鎖的信息。
這里可以找到線程私鎖的詳細信息,具體內容和上面使用Jstack命令查詢的結果一樣,這里實用工具更加方便。
4、JVM參數調優
jvm調優通常使用的是Jstat命令。
1. 垃圾回收統計 jstat -gc
jstat -gc 進程id
這個命令非常常用,在線上有問題的時候,可以通過這個命令來分析問題。
下面我們來測試一下,啟動一個項目,然后在終端駛入jstat -gc 進程id,得到如下結果:
上面的參數分別是什么意思呢?先識別參數的含義,然后根據參數進行分析
- 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
這樣就連續打印了10次gc的變化,每次隔一秒。
這個命令是對整體垃圾回收情況的統計,下面將會差分處理。
2.堆內存統計
這個命令是打印堆內存的使用情況。
jstat -gccapacity 進程ID
- 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的內存情況
- S0C:第一個Survivor的大小
- S1C:第二個Survivor的大小
- S0U:第一個Survivor已使用大小
- S1U:第二個Survivor已使用大小
- TT: 對象在新生代存活的次數
- MTT: 對象在新生代存活的最大次數
- DSS: 期望的Survivor大小
- EC:Eden區的大小
- EU:Eden區的使用大小
- YGC:年輕代垃圾回收次數
- YGCT:年輕代垃圾回收消耗時間
4. 新生代內存統計
jstat -gcnewcapacity 進程ID
參數含義:
- 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
參數含義:
- MC:方法區大小
- MU:方法區已使用大小
- CCSC:壓縮指針類空間大小
- CCSU:壓縮類空間已使用大小
- OC:老年代大小
- OU:老年代已使用大小
- YGC:年輕代垃圾回收次數
- FGC:老年代垃圾回收次數
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間,新生代+老年代
6. 老年代內存統計
命令:
jstat -gcoldcapacity 進程ID
參數含義:
- OGCMN:老年代最小容量
- OGCMX:老年代最大容量
- OGC:當前老年代大小
- OC:老年代大小
- YGC:年輕代垃圾回收次數
- FGC:老年代垃圾回收次數
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
7. 元數據空間統計
命令
jstat -gcmetacapacity 進程ID
- MCMN:最小元數據容量
- MCMX:最大元數據容量
- MC:當前元數據空間大小
- CCSMN:最小指針壓縮類空間大小
- CCSMX:最大指針壓縮類空間大小
- CCSC:當前指針壓縮類空間大小
- YGC:年輕代垃圾回收次數
- FGC:老年代垃圾回收次數
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
8.整體運行情況
命令:
jstat -gcutil 進程ID
- S0:Survivor 1區當前使用比例
- S1:Survivor 2區當前使用比例
- E:Eden區使用比例
- O:老年代使用比例
- M:元數據區使用比例
- CCS:指針壓縮使用比例
- YGC:年輕代垃圾回收次數
- YGCT:年輕代垃圾回收消耗時間
- FGC:老年代垃圾回收次數
- FGCT:老年代垃圾回收消耗時間
- GCT:垃圾回收消耗總時間
通過查詢上面的參數來分析整個堆空間。
二、Arthas線上分析工具的使用
Arthas的功能非常強大,現附上官方文檔:https://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 https://alibaba.github.io/arthas/arthas-boot.jar
也可以在瀏覽器直接訪問https://alibaba.github.io/arthas/arthas-boot.jar,等待下載成功后,上傳到Linux服務器上。
2) 啟動
執行命令就可以啟動了
java -jar arthas-boot.jar
啟動成功可以看到如下界面:
然后找到你想監控的進程,輸入前面對應的編號,就可以開啟進行監控模式了。比如我要看4
看到這個就表示,進入應用監聽成功
2、dashboard儀表盤--查詢整體項目運行情況
執行命令
dashboard
這里面一共有三塊
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
Load < 1
Load = 1
Load >1
> 多核處理器
我們經常會發現服務器Load > 1但是運行仍然不錯,那是因為服務器是多核處理器(Multi-core)。
假設我們服務器CPU是2核,那么將意味我們擁有2條馬路,我們的Load = 2時,所有馬路都跑滿車輛。
Load = 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
從線程的詳情可以分析出,目前第一個線程的使用率是最高的,cpu占用了達到99.38%。第二行告訴我們,是Arthas.java這個類的第38行導致的。
由此,我們可以一眼看出問題,然后定位問題代碼的位置,接下來就是人工排查問題了。
2、查詢出當前被阻塞的線程
命令:
thread -b
可以看到內容提示,線程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
運行結果:
運行結果分析:這里包含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()'
其中,第一個@后面跟的是類的全名稱;第二個@跟的是屬性或者方法名,如果屬性是一個對象,想要獲取屬性里面的屬性或者方法,直接打.屬性名/方法名 即可。
運行效果:
我們看到了這個對象的返回值是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
返回值
* 案例2:返回對象中具體參數的值。加 -x 2
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' -x 2
返回值
* 案例3:返回對象中有其他對象
- 命令:
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' -x 2
執行結果:
-x 2 獲取的是對象的值,List返回的是數組信息,數組長度。
- 命令:
ognl '@com.lxl.jvm.DeadLockTest@addUsers(1,"zhangsan")' -x 3
執行結果:
-x 3 打印出對象的值,對象中List列表中的值。
* 案例4:方法A的返回值當做方法B的入參
ognl '#value1=@com.lxl.jvm.DeadLockTest@getCitys(), #value2=@com.lxl.jvm.DeadLockTest@generatorUser(1,"lisi",#value1), {#value1,#value2}' -x 2
> 方法入參是簡單類型的列表
ognl '@com.lxl.jvm.DeadLockTest@returnCitys({"beijing","shanghai","guangdong"})'
> 方法入參是一個復雜對象
ognl '#value1=new com.lxl.jvm.User(1,"zhangsan"),#value1.setName("aaa"), #value1.setCitys({"bj", "sh"}), #value2=@com.lxl.jvm.DeadLockTest@addUsers(#value1), #value2' -x 3
> 方法入參是一個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
2)獲取靜態字段
ognl '@全路徑類名@靜態屬性名'
示例:在DeadLockTest類中有一個names靜態屬性,下面來看看如何獲取這個靜態屬性。執行命令:
ognl '@com.lxl.jvm.DeadLockTest@names'
其中,第一個@后面跟的是類的全名稱;第二個@跟的是屬性或者方法名,如果屬性是一個對象,想要獲取屬性里面的屬性或者方法,直接打.屬性名/方法名 即可。
運行效果:
第一次執行獲取屬性命令,返回的屬性是一個空集合;然后執行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關鍵字,執行結果:
7. 線上代碼修改
生產環境有時會遇到非常緊急的問題,或突然發現一個bug,這時候不方便重新發版,或者發版未生效,可以使用Arthas臨時修改線上代碼。通過Arthas修改的步驟如下:
1. 從讀取.class文件
2. 編譯成.java文件
3. 修改.java文件
4. 將修改后的.java文件編譯成新的.class文件
5. 將新的.class文件通過classloader加載進JVM內
第一步:讀取.class文件
sc -d *DeadLockTest*
使用sc命令查看JVM已加載的類信息。關於sc命令,查看官方文檔:https://arthas.aliyun.com/doc/sc.html
- -d : 表示打印類的詳細信息
最后一個參數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文件, 詳細使用方法參考官方文檔https://arthas.aliyun.com/doc/mc.html
- -c:指定classloader的hash值
- -d:指定輸出目錄
- 最后一個參數是java文件路徑
這是反編譯后的class字節碼文件
第五步:使用redefine命令,將.class文件重新加載進JVM
redefine -c /Users/***/Downloads/com/lxl/jvm/DeadLockTest.class
最后看到redefine success,表示重新加載.class文件進JVM成功了。
注意事項
redefine命令使用之后,再使用jad命令會使字節碼重置,恢復為未修改之前的樣子。官方關於redefine命令的說明
第六步:檢驗效果
這里檢測效果,調用接口,執行日志即可。
8、實時修改生產環境的日志級別
這個功能也很好用,通常,我們在日志中打印的日志級別一般是infor、warn、error級別的,debug日志一般看不到。那么出問題的時候,一些日志,在寫代碼的時候會被記錄在debug日志中,而此時日志級別又很高。那么迫切需要調整日志級別。
這個功能很好用啊,我們可以將平時不經常打印出來的日志設置為debug級別。設置線上日志打印級別為info。當線上有問題的時候,可以將日志級別動態調整為debug。異常排查完,在修改回info。這對訪問量特別大日志內容很多的項目比較有效,可以有效節省日志輸出帶來的開銷。
第一步:使用logger命令查看日志級別
- 當前應用的日志級別是info
- 類加載的hash值是18b4aac2
我們定義一個接口,其源代碼內容如下:
@PostMapping(value = "test")
public String test() {
log.debug("這是一條 debug 級別的日志");
log.info("這是一條 info 級別的日志");
log.error("這是一條 error 級別的日志");
log.warn("這是一條 warn 級別的日志");
return "完成";
}
可以調用接口,查看日志輸出代碼。
我們看到,日志輸出的是info及以下的級別。
第二步:修改logger日志的級別
logger -c 18b4aac2 --name ROOT --level debug
修改完日志級別以后,輸出日志為debug級別。
8. 查詢jvm信息,並修改----當發生內存溢出時,可以手動設置打印堆日志到文件
通常查詢jvm參數,使用的是Java自帶的工具[jinfo 進程號]。arthas中通過vmoption獲取jvm參數:
假設,我們要設置JVM出現OutOfMemoryError的時候,自動dump堆快照
vmoption HeapDumpOnOutOfMemoryError true
這時,如果發生堆內存溢出,會打印日志到文件
9. 監控函數耗時
trace 待監控方法的全類名 待監控的方法名
trace com.lxl.jvm.DeadLockTest generatorUser
- 通過圈起來的部分可以看到,接口的入口函數time總耗時371ms
- 其中getDataFromDb函數耗時200ms
- getDataFromRedis函數耗時100ms
- getDataFromOuter函數耗時50ms
- process函數耗時20ms
很明顯,最慢的函數已經找到了,接下里就要去對代碼進行進一步分析,然后再進行優化