帶著8個(gè)問題5分鐘教你學(xué)會(huì) Arthas 診斷工具
點(diǎn)擊關(guān)注公眾號(hào),Java干貨及時(shí)送達(dá)??

前言
Arthas?是Alibaba開源的Java診斷工具,深受開發(fā)者喜愛。
當(dāng)你遇到以下類似問題而束手無策時(shí),Arthas可以幫助你解決:
這個(gè)類從哪個(gè) jar 包加載的?為什么會(huì)報(bào)各種類相關(guān)的 Exception? 我改的代碼為什么沒有執(zhí)行到?難道是我沒 commit?分支搞錯(cuò)了? 遇到問題無法在線上 debug,難道只能通過加日志再重新發(fā)布嗎? 線上遇到某個(gè)用戶的數(shù)據(jù)處理有問題,但線上同樣無法 debug,線下無法重現(xiàn)! 是否有一個(gè)全局視角來查看系統(tǒng)的運(yùn)行狀況? 有什么辦法可以監(jiān)控到JVM的實(shí)時(shí)運(yùn)行狀態(tài)? 怎么快速定位應(yīng)用的熱點(diǎn),生成火焰圖? 怎樣直接從JVM內(nèi)查找某個(gè)類的實(shí)例?
這 8 個(gè)問題,Arthas 官方文檔(https://arthas.aliyun.com/doc)中并沒有給出答案或標(biāo)準(zhǔn)的解決方案。

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

正文
「下面是筆者結(jié)合多年使用 Arthas 的經(jīng)驗(yàn),針對(duì)這 8 個(gè)問題給出的詳細(xì)解決方案,如果有疑問歡迎評(píng)論區(qū)指出?!?/strong>
準(zhǔn)備
先給出我的測(cè)試代碼
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?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?interests;
????}
}
以下是控制臺(tái)正常打印的結(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])]
下載并運(yùn)行 Arthas
按照下圖中的步驟,選擇一個(gè) Java 進(jìn)程進(jìn)行 attach。

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

classloader
通過?classloader?查看 class 文件來自哪個(gè) jar 包
?使用?
?cls?命令可以清空命令行,這個(gè)簡(jiǎn)單的命令官方文檔居然找不到。。。
?注意?
?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?分支搞錯(cuò)了?
推薦使用?watch?和?tt?命令,非常好用。
這兩個(gè)命令都是用來查看方法調(diào)用過程的,不同的是?watch?命令是調(diào)用一次打印一次方法的調(diào)用情況,而?tt?命令可以先生成一個(gè)不斷增加的調(diào)用列表,然后指定其中某一項(xiàng)進(jìn)行觀測(cè)。
使用? watch?命令查看方法調(diào)用情況。我們要查看 ArthasDemo 這個(gè)類里面的 convert 方法調(diào)用情況。

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

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

?圖中之所以可以打印興趣列表,是調(diào)用了其 toString 方法,如果沒有重寫 java.lang.Object 類的 toString 方法,只會(huì)看到 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?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?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?getInterests()?{
???????????????????return?this.interests;
???????????????}
???????????????public?void?setAge(String?age)?{
???????????????????this.age?=?age;
???????????????}
???????????????public?void?setTelephone(String?telephone)?{
???????????????????this.telephone?=?telephone;
???????????????}
???????????????public?void?setInterests(List?interests)?{
???????????????????this.interests?=?interests;
???????????????}
???????????}
???????}
[arthas@3633]$
問題 3:遇到問題無法在線上 debug,難道只能通過加日志再重新發(fā)布嗎?
通過上面問題 2 的?watch?和?tt?命令可以查看方法調(diào)用情況。
此外,可以通過?redefine?命令「熱替換」線上的代碼,注意應(yīng)用重啟之后會(huì)失效,這在某些緊急情況下會(huì)有奇效。
比如說我們修改一下方法體里面的代碼,加了一行日志打印:
????private?List?convert(String?s)? {
????????System.out.println(s);
????????return?JSON.parseArray(s,?People.class);
????}
這時(shí)我們就可以將新代碼編譯后的 class 文件熱替換正在運(yùn)行的 ArthasDemo 的代碼。


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

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

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

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

問題 7:怎么快速定位應(yīng)用的熱點(diǎn),生成火焰圖?
profiler?命令支持生成應(yīng)用熱點(diǎn)的火焰圖。本質(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é)果:

選擇一項(xiàng)點(diǎn)擊

問題 8:怎樣直接從JVM內(nèi)查找某個(gè)類的實(shí)例?
使用?vmtool?可以達(dá)成目的
?這個(gè)功能是 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ù)時(shí)對(duì)JVM造成壓力。默認(rèn)值是10。
如果想精確的定位到具體的類實(shí)例,可以通過指定 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 的方法請(qǐng)參考上面的問題 1
?
vmtool 還有個(gè)不錯(cuò)的功能,可以「強(qiáng)制進(jìn)行GC」,這在某些生產(chǎn)環(huán)境內(nèi)存緊張的情況下有奇效。
vmtool?--action?forceGc
??
1.?圖文并茂,Spring Boot Starter 萬字詳解!還有誰(shuí)不會(huì)?
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊(cè)》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫(kù)、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點(diǎn)“在看”,關(guān)注公眾號(hào)并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)

