帶著8個問題5分鐘教你學(xué)會Arthas診斷工具
你知道的越多,不知道的就越多,業(yè)余的像一棵小草!
成功路上并不擁擠,因為堅持的人不多。
編輯:業(yè)余草
blog.csdn.net/Shockang
推薦:https://www.xttblog.com/?p=5247
前言
Arthas 是Alibaba開源的Java診斷工具,深受開發(fā)者喜愛。
當(dāng)你遇到以下類似問題而束手無策時,Arthas可以幫助你解決:
這個類從哪個 jar 包加載的?為什么會報各種類相關(guān)的 Exception? 我改的代碼為什么沒有執(zhí)行到?難道是我沒 commit?分支搞錯了? 遇到問題無法在線上 debug,難道只能通過加日志再重新發(fā)布嗎? 線上遇到某個用戶的數(shù)據(jù)處理有問題,但線上同樣無法 debug,線下無法重現(xiàn)! 是否有一個全局視角來查看系統(tǒng)的運行狀況? 有什么辦法可以監(jiān)控到JVM的實時運行狀態(tài)? 怎么快速定位應(yīng)用的熱點,生成火焰圖? 怎樣直接從JVM內(nèi)查找某個類的實例?
這 8 個問題,Arthas 官方文檔(https://arthas.aliyun.com/doc)中并沒有給出答案或標(biāo)準(zhǔn)的解決方案。

這不是管殺不管埋嗎!!!

正文
「下面是筆者結(jié)合多年使用 Arthas 的經(jīng)驗,針對這 8 個問題給出的詳細(xì)解決方案,如果有疑問歡迎評論區(qū)指出。」
準(zhǔn)備
先給出我的測試代碼
package com.shockang.study;
import com.alibaba.fastjson.JSON;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.FieldDefaults;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ArthasDemo {
public static void main(String[] args) {
String s = "[{\"name\":\"zhangsan\",\"age\":\"10\",\"telephone\":\"123456\",\"interests\":[\"sing\",\"dance\",\"rap\"]},\n" +
"{\"name\":\"lisi\",\"age\":\"20\",\"telephone\":\"123457\",\"interests\":[\"sing\",\"swim\"]},\n" +
"{\"name\":\"wangwu\",\"age\":\"30\",\"telephone\":\"123458\",\"interests\":[\"sing\",\"program\"]}]";
//模擬一遍遍的調(diào)用方法的過程
for (; ; ) {
System.out.println(new ArthasDemo().convert(s));
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private List<People> convert(String s) {
return JSON.parseArray(s, People.class);
}
@Getter
@Setter
@ToString
@FieldDefaults(level = AccessLevel.PRIVATE)
private static class People {
/**
* 姓名
*/
String name;
/**
* 年齡
*/
String age;
/**
* 電話
*/
String telephone;
/**
* 興趣列表
*/
List<String> interests;
}
}
以下是控制臺正常打印的結(jié)果
/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/bin/java ...
[ArthasDemo.People(name=zhangsan, age=10, telephone=123456, interests=[sing, dance, rap]), ArthasDemo.People(name=lisi, age=20, telephone=123457, interests=[sing, swim]), ArthasDemo.People(name=wangwu, age=30, telephone=123458, interests=[sing, program])]
[ArthasDemo.People(name=zhangsan, age=10, telephone=123456, interests=[sing, dance, rap]), ArthasDemo.People(name=lisi, age=20, telephone=123457, interests=[sing, swim]), ArthasDemo.People(name=wangwu, age=30, telephone=123458, interests=[sing, program])]
下載并運行 Arthas
按照下圖中的步驟,選擇一個 Java 進程進行 attach。

訪問 WebConsole
attach 成功后可以打開谷歌瀏覽器輸入http://127.0.0.1:3658/ 打開 WebConsole
(吐槽一句 Mac OS 的 Safari 瀏覽器不支持)
?使用 WebConsole 最方便的是你可以打開多個標(biāo)簽頁同時操作
?
問題 1:這個類從哪個 jar 包加載的?為什么會報各種類相關(guān)的 Exception?
這個問題我經(jīng)常在處理各種「依賴沖突」的時候遇到,有一些類的完全名稱是一模一樣,通過常規(guī)的辦法無法解決類具體從哪個 jar 包加載。
別急,看我下面的解決辦法。
sc
通過 sc 命令 模糊查看當(dāng)前 JVM 中是否加載了包含關(guān)鍵字的類,以及獲取其完全名稱。
?注意使用
?sc -d命令,獲取 classLoaderHash,這個值在后面需要用到。
sc -d *ArthasDemo*

classloader
通過 classloader 查看 class 文件來自哪個 jar 包
?使用
?cls命令可以清空命令行,這個簡單的命令官方文檔居然找不到。。。
?注意
?classloader -c后面的值填上面第一步中獲取到的 Hash 值,class 文件路徑使用'/'分割,且必須以.class 結(jié)尾。
[arthas@3633]$ classloader -c 18b4aac2 -r com/shockang/study/ArthasDemo.class
file:/Users/shockang/code/concurrentbook/target/classes/com/shockang/study/ArthasDemo.class
Affect(row-cnt:1) cost in 0 ms.
上面是顯示 class 文件路徑的,如果 class 文件來自 jar 包,可以顯示 jar 包路徑,例如官方文檔給的例子:
$ classloader -c 1b6d3586 -r java/lang/String.class
jar:file:/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre/lib/rt.jar!/java/lang/String.class
問題 2:我改的代碼為什么沒有執(zhí)行到?難道是我沒 commit?分支搞錯了?
推薦使用 watch 和 tt 命令,非常好用。
這兩個命令都是用來查看方法調(diào)用過程的,不同的是 watch 命令是調(diào)用一次打印一次方法的調(diào)用情況,而 tt 命令可以先生成一個不斷增加的調(diào)用列表,然后指定其中某一項進行觀測。
使用 watch命令查看方法調(diào)用情況。我們要查看 ArthasDemo 這個類里面的 convert 方法調(diào)用情況。

watch com.shockang.study.ArthasDemo convert "{params,target,returnObj}" -f -x 4
watch 后面跟上完全類名和方法名,以及一個 OGNL 的表達(dá)式,-f 表示不論正常返回還是異常返回都進行觀察,-x 表示輸出結(jié)果的屬性遍歷深度,默認(rèn)為 1,
?建議無腦寫 4 就行,這是筆者經(jīng)驗來看最大的遍歷深度,再大就不支持了
?
使用 tt命令來觀測方法調(diào)用情況,tt命令可以查看「多次調(diào)用」并選擇其中一個進行觀測,但是如果輸出結(jié)果是多層嵌套就沒辦法看了,而watch可以查看「多層嵌套」的結(jié)果。
?使用 tt -t 記錄下當(dāng)前方法的每次調(diào)用環(huán)境現(xiàn)場
?

tt -t com.shockang.study.ArthasDemo convert
TIMESTAMP表示方法調(diào)用發(fā)生的時間,COST 表示調(diào)用耗時(ms),IS-RET表示是否正常返回,IS-EXP 表示是否異常返回,OBJECT 表示對象的 HASH 值
?對于具體一個時間片的信息而言,你可以通過 -i 參數(shù)后邊跟著對應(yīng)的 INDEX 編號查看到他的詳細(xì)信息
?

?圖中之所以可以打印興趣列表,是調(diào)用了其 toString 方法,如果沒有重寫 java.lang.Object 類的 toString 方法,只會看到 hash 值。
?
如何判斷代碼是否已經(jīng)提交?
通過 jad --source-only 可以查看源代碼。
[arthas@3633]$ jad --source-only com.shockang.study.ArthasDemo
/*
* Decompiled with CFR.
*/
package com.shockang.study;
import com.alibaba.fastjson.JSON;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class ArthasDemo {
public static void main(String[] args) {
/*15*/ String s = "[{\"name\":\"zhangsan\",\"age\":\"10\",\"telephone\":\"123456\",\"interests\":[\"sing\",\"dance\",\"rap\"]},\n{\"name\":\"lisi\",\"age\":\"20
\",\"telephone\":\"123457\",\"interests\":[\"sing\",\"swim\"]},\n{\"name\":\"wangwu\",\"age\":\"30\",\"telephone\":\"123458\",\"interests\":[\"sing\",\"program\"]}]";
while (true) {
/*20*/ System.out.println(new ArthasDemo().convert(s));
try {
/*22*/ TimeUnit.SECONDS.sleep(10L);
/*25*/ continue;
}
catch (InterruptedException e) {
/*24*/ e.printStackTrace();
continue;
}
break;
}
}
private List<People> convert(String s) {
/*30*/ return JSON.parseArray(s, People.class);
}
private static class People {
private String name;
private String age;
private String telephone;
private List<String> interests;
private People() {
}
public String toString() {
return "ArthasDemo.People(name=" + this.getName() + ", age=" + this.getAge() + ", telephone=" + this.getTelephone() + ", interests=" + this.getIntere
sts() + ")";
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getAge() {
return this.age;
}
public String getTelephone() {
return this.telephone;
}
public List<String> getInterests() {
return this.interests;
}
public void setAge(String age) {
this.age = age;
}
public void setTelephone(String telephone) {
this.telephone = telephone;
}
public void setInterests(List<String> interests) {
this.interests = interests;
}
}
}
[arthas@3633]$
問題 3:遇到問題無法在線上 debug,難道只能通過加日志再重新發(fā)布嗎?
通過上面問題 2 的 watch 和 tt 命令可以查看方法調(diào)用情況。
此外,可以通過 redefine 命令「熱替換」線上的代碼,注意應(yīng)用重啟之后會失效,這在某些緊急情況下會有奇效。
比如說我們修改一下方法體里面的代碼,加了一行日志打印:
private List<People> convert(String s) {
System.out.println(s);
return JSON.parseArray(s, People.class);
}
這時我們就可以將新代碼編譯后的 class 文件熱替換正在運行的 ArthasDemo 的代碼。


從這張圖可以明顯的看出,明明源碼中沒有打印字符串 s 的邏輯,但是控制臺還是打印了字符串,因為我們已經(jīng)熱替換了 JVM 內(nèi)存中(方法區(qū))加載的類。
問題 4:線上遇到某個用戶的數(shù)據(jù)處理有問題,但線上同樣無法 debug,線下無法重現(xiàn)!
這個問題沒有完美的解決辦法
參考一下問題 2 和問題 3的解決方案
推薦使用 tt 命令并將命令行返回結(jié)果輸出到一個文件中,后續(xù)可以選擇異常的一行記錄使用 tt -i 命令進行深入的分析。
tee指令會從標(biāo)準(zhǔn)輸入設(shè)備讀取數(shù)據(jù),將其內(nèi)容輸出到標(biāo)準(zhǔn)輸出設(shè)備,同時保存成文件。

tt -t com.shockang.study.ArthasDemo convert | tee /Users/shockang/Downloads/log
此外還可以使用 monitor 命令統(tǒng)計方法調(diào)用成功失敗情況。

monitor -c 30 com.shockang.study.ArthasDemo convert | tee /Users/shockang/Downloads/log1
?-c 后面接統(tǒng)計周期,默認(rèn)值為120秒
?
問題 5:是否有一個全局視角來查看系統(tǒng)的運行狀況?
使用 dashboard 命令可以查看當(dāng)前系統(tǒng)的實時數(shù)據(jù)面板, 當(dāng)運行在Ali-tomcat時,會顯示當(dāng)前tomcat的實時信息,如HTTP請求的qps, rt, 錯誤數(shù), 線程池信息等等。

從圖中可以看到線程情況,內(nèi)存使用情況,系統(tǒng)參數(shù)等。
問題 6:有什么辦法可以監(jiān)控到JVM的實時運行狀態(tài)?
使用 jvm 命令可以查看 JVM 的實時運行狀態(tài)。

問題 7:怎么快速定位應(yīng)用的熱點,生成火焰圖?
profiler 命令支持生成應(yīng)用熱點的火焰圖。本質(zhì)上是通過不斷的采樣,然后把收集到的采樣結(jié)果生成火焰圖。
?默認(rèn)情況下,生成的是 cpu 的火焰圖,即 event 是 cpu,可以用--event 參數(shù)來指定。注意不同系統(tǒng)支持的 event 不同
?
默認(rèn)情況下,arthas使用3658端口,則可以打開:http://localhost:3658/arthas-output/ 查看到arthas-output目錄下面的profiler結(jié)果:

選擇一項點擊

問題 8:怎樣直接從JVM內(nèi)查找某個類的實例?
使用 vmtool 可以達(dá)成目的
?這個功能是 Arthas 3.5.1 新增的。可以參考官方文檔 https://arthas.aliyun.com/doc/vmtool.html#id1
?
$ vmtool --action getInstances --className java.lang.String --limit 10
@String[][
@String[com/taobao/arthas/core/shell/session/Session],
@String[com.taobao.arthas.core.shell.session.Session],
@String[com/taobao/arthas/core/shell/session/Session],
@String[com/taobao/arthas/core/shell/session/Session],
@String[com/taobao/arthas/core/shell/session/Session.class],
@String[com/taobao/arthas/core/shell/session/Session.class],
@String[com/taobao/arthas/core/shell/session/Session.class],
@String[com/],
@String[java/util/concurrent/ConcurrentHashMap$ValueIterator],
@String[java/util/concurrent/locks/LockSupport],
]
通過 --limit參數(shù),可以限制返回值數(shù)量,避免獲取超大數(shù)據(jù)時對JVM造成壓力。默認(rèn)值是10。
如果想精確的定位到具體的類實例,可以通過指定 classloader name 或者 classloader hash,如下所示:
vmtool --action getInstances --classLoaderClass org.springframework.boot.loader.LaunchedURLClassLoader --className org.springframework.context.ApplicationContext
vmtool --action getInstances -c 19469ea2 --className org.springframework.context.ApplicationContext
?獲取 classloader hash 的方法請參考上面的問題 1
?
vmtool 還有個不錯的功能,可以「強制進行GC」,這在某些生產(chǎn)環(huán)境內(nèi)存緊張的情況下有奇效。
vmtool --action forceGc
