一、簡介
Arthas是Alibaba開源的一款Java診斷工具,采用命令式交互模式,用來排查各種JVM的問題。
Arthas主要提供了一下幾種功能
1、實時監控JVM運行狀況
2、實時查看已加載的類型和類加載器信息
3、通過字節碼增強技術實現方法執行的監控和統計
二、Arthas的使用
2.1、Arthas安裝啟動
Arthas本質上也是一個Java程序,沒有安裝流程,只需要下載jar包,通過java命令直接啟動即可
下載arthas-boot.jar
,然后用java -jar
的方式啟動:
下載命令:
wget https://arthas.aliyun.com/arthas-boot.jar
啟動命令:
java -jar arthas-boot.jar
啟動之后會列出當前節點正在運行的所有Java進程,並且有對應的序號,可以輸入序號來選擇需要訪問的進程信息,如選擇第一個進程就直接輸入1即可,啟動效果如下圖示:
表示Attach上進程號為21093的Java進程,並且此時就進入Arthas的命令交互模式,不可以再輸入操作系統的命令,只可以輸入Arthas相關的命令,可以輸入help命令查看所有Arthas支持的命令,如下:
2.2、Arthas命令概覽
Arthas命令主要分成幾個類型,分別是基本命令、JVM相關命令、class相關命令、監控相關命令
2.2.1、基本命令
命令 | 用途 |
help | 查看命令幫助信息 |
cat | 打印文件內容 |
echo | 打印參數 |
grep | 匹配查找 |
base64 | base64編碼轉換 |
tee | 復制標志輸入到標准輸出和指定的文件 |
pwd | 返回當前工作目錄 |
cls | 清空屏幕內容 |
session | 查看當前會話信息 |
reset | 重置增強類,將被Arthas增強過的類全部還原,Arthas服務關閉時自動還原 |
version | 輸出當前目標Java進程加載的Arthas版本號 |
history | 打印命令歷史 |
quit | 退出當前Arthus客戶端 |
stop | 關閉Arthas服務端,所有Arthas客戶端全部退出 |
2.2.2、JVM相關命令
命令 | 用途 | 用法 |
dashboard | 打印當前JVM實時數據面板 | dashboard |
thread | 打印當前JVM線程堆棧信息 | thread thread 線程ID thread --state WAITING |
jvm |
打印當前JVM實時運行信息 | jvm |
sysprop | 打印和修改當前JVM信息 | sysprop |
sysenv | 查看當前JVM環境變量 | sysenv |
vmoption | 查看和修改JVM里診斷相關option | vmoption |
logger | 查看和修改logger信息 | logger |
getstatic | 獲取靜態變量的值 | getstatic [className] [staticField] |
ognl | 執行ognl表達式 | ognl 表達式 |
mbean | 打印MBean信息 | mbean |
heapdump | 打印堆棧信息 | heapdump heapdump /test/dump/test.hprof heapdump --live /test/dump/test.hprof |
vmtool | 從jvm里查詢對象,執行forceGc | vmtool --action getInstances --className [className] |
perfcounter | 查看當前JVM的 Perf Counter信息 | perfcounter |
2.2.3、class相關命令
命令 | 用途 | 用法 |
sc | 查詢JVM已加載的類信息 | sc *[className]* sc -d *[className]* sc -d -f *[className]* |
sm | 查詢JVM已加載的方法信息 | sm [className] sm -f [className] |
jad | 反編譯JVM已加載的類信息 | jad [className] |
mc | 內存編譯器,通過內存編譯.java文件 | mc /com/test/test.java mc -c [類加載ID] /com/test/test.java mc --classLoadClass [classLoadClassName] /com/test/test.java mc -d /tmp/file /com/test/test.java |
retransform | 加載外部.class文件,retransform到JVM中 | retransform /com/test/test.class |
redefine | 加載外部.class文件,redefine到JVM中 | redefine /com/test/test/class |
dump | dump已加載類的字節碼到指定目錄 | dump java.lang.String dump -d /tmp/file java.lang.String |
classloader | 查看類加載器繼承樹信息 | classloader classloader -l classloader -t classloader -c [類加載ID] --load [className] |
2.2.4、監控相關命令
監控相關的命令是通過字節碼增強技術將增強的邏輯織入到目標類中,所以監控完成之后需求及時執行reset命令去除增強的邏輯
命令 | 用途 | 用法 |
watch | 方法執行數據觀測 | watch [className] [methodName] "{params,returnObj}" -x 2 |
monitor | 方法執行監控 | monitor -c 5 [className] [methodName] |
stack | 輸出當前方法被調用的路徑 | stack [className] [methodName] |
trace | 方法內部的調用路徑,並打印路徑耗時 | trace [className] [methodName] |
tt | 方法執行數據的時空隧道,記錄下指定方法每次調用的入參和返回信息,並能對這些不同的時間下調用進行觀測 | tt -t [className] [methodName] |
2.3、Arthas命令詳解
2.3.1、dashborad(實時看板)
語法:dashboard [i:] [n:]
dashboard ##默認每個5秒打印一次JVM實時數據 dashboard -i 2000 ##每隔2秒打印一次JVM實時數據 dashboard -n 10 ##總共打印10次JVM實時數據
dashboard -i 1000 -n 10 ##每隔1秒打印一次JVM實時數據,共打印10次
dashboard命令用於實時查看JVM運行信息,包括三個模塊分布是線程運行情況、內存使用情況以及JVM運行環境信息等
線程模塊會打印當前JVM所有的線程運行狀況,包括線程ID、名稱、優先級、狀態、占有CPU比率、占有CPU時長等;
內存模塊會打印JVM當前堆內和堆外內存使用情況以及GC的次數和時間統計
運行環境模塊會打印當前操作系統和JDK的版本信息以及運行環境等信息
2.3.2、thread(線程信息)
通過thread命令可以打印當前所有運行的線程信息,並且可以通過thread 線程ID的方式查看指定線程的棧信息
語法
thread 查詢所有線程
thread <id> 查詢指定線程
thread -n <n> 查詢最忙的n個線程
thread -i <i>
: 統計最近指定毫秒內的線程CPU時間
thread -n <n> -i <i>
: 列出最近指定毫秒內最忙的N個線程棧
thread -b 查詢阻塞其他線程的線程
thread --state [RUNNABEL]|[WAITING]|[TIMED_WAITING]|[NEW]|[BLOCKED]|[TERMINATED] 查詢指定狀態的線程
thread ## 查看所有線程信息
thread <id> ## 查看指定線程ID的信息
thread -n 10 ## 查看最忙的10個線程
thread -b ## 查看阻塞其他線程的線程信息
thread --state WAITING ## 查看等待狀態的線程信息
通過thread查看所有線程信息,除了業務線程還會包含JVM內部線程,JVM內部線程ID為-1,包括GC線程如GC task thread#2 (ParallelGC)、JIT編譯線程如C2 CompilerThread0、其他內部線程如VM Periodic Task Thread等
thread -b 命令可以找出當前阻塞了其他線程的線程,比如線程A通過Synchronized關鍵字拿到了鎖,線程B和線程C被阻塞了,那么此時就可以通過thread -b命令查詢出來,目前也僅支持Synchronized獲取鎖阻塞的情況
2.3.3、jvm(查看JVM信息)
用法:jvm
jvm命令可以打印當前JVM的實時信息,包括運行環境、加載的類統計、內存使用情況、GC統計情況、線程統計情況、文件描述符統計信息等
THREAD相關
-
COUNT: JVM當前活躍的線程數
-
DAEMON-COUNT: JVM當前活躍的守護線程數
-
PEAK-COUNT: 從JVM啟動開始曾經活着的最大線程數
-
STARTED-COUNT: 從JVM啟動開始總共啟動過的線程次數
-
DEADLOCK-COUNT: JVM當前死鎖的線程數
文件描述符相關
-
MAX-FILE-DESCRIPTOR-COUNT:JVM進程最大可以打開的文件描述符數
-
OPEN-FILE-DESCRIPTOR-COUNT:JVM當前打開的文件描述符數
2.3.4、jad(反編譯)
語法
jad <類完整路徑> 反編譯指定類
jad <類完整路徑> <方法名> 反編譯指定方法
通過sc可以查看所有已加載的類信息,然后就可以通過jad命令可以將已加載的類.class文件進行反編譯,命令如下:
jad com.test.ArthasDemo
2.3.5、sc(查看已加載的類)
語法
sc [-d] [-f] *<className>*
[-d] 表示打印詳細信息;[-f]表示打印屬性信息,可以通過關鍵字模糊查詢
sc是search class的縮寫,這個命令能搜索出所有已經加載到 JVM 中的 Class 信息,這個命令支持的參數有 [d]
、[E]
、[f]
和 [x:],並且支持模糊查詢
如想搜索后綴為Service的類,則可以輸入一下命令:
sc -d -f *Service
-d表示輸出當前類的詳細信息,包括這個類所加載的原始文件來源、類的聲明、加載的ClassLoader等詳細信息。如果一個類被多個ClassLoader所加載,則會出現多次
-f表示輸出當前類的成員變量信息(需要配合參數-d一起使用)
通過該命令可以查看已加載的類的詳細信息,包括code-source表示從哪個包中加載的,使用哪個class-loader類加載器加載的等,案例如下圖示:
2.3.6、sm(查看已加載類的方法)
用法
sm <類完整路徑>
sm -d <類完整路徑>
sm -d <類完整路徑> <方法名>
sm命令可以查看已加載的類的函數,比如查看java.math.RoundingMode類的所有方法,則命令如下:
sm -d java.math.RoundingMode
2.3.7、getstatic(查看靜態屬性)
用法
getstatic <類完整路徑> <靜態屬性名>
通過getstatic命令可以查看已加載的類的靜態屬性的值
2.3.8、watch(監控方法執行數據)
用法
watch <className> <method> 觀察指定類指定方法的執行情況,返回耗時,返回值等信息
watch <className> <method> "{params,returnObj}"-x <x> 觀察入參和出參,返回結果便利深度為x,默認深度為1
watch <className> <method> "{params,returnObj}"-b 觀測方法調用前的入參和返回值,調用方法前返回值肯定為空
watch <className> <method> "{params,returnObj}" -b -s -n <n> 同時觀察方法執行前和方法執行后的入參和返回值,監控n次
watch <className> <method> "{params,throwExp}" -e 觀察方法在拋出異常時的入參和異常信息
watch <className> <method> "{params,target}" -b -s 觀察方法執行前和方法執行后入參和當前對象的信息
watch <className> <method> "{params,target.fieldName}" -b -s 觀察方法執行前和方法執行后入參和當前對象的fieldName屬性的值
watch <className> <method> "{params}" '#cost>100' -f 觀察方法執行后執行耗時大於100毫秒的入參信息
通過watch命令可以監控指定類的指定方法的參數、返回值和異常信息,watch 命令定義了4個觀察事件點,即 -b
方法調用前,-e
方法異常后,-s
方法返回后,-f
方法結束后,
4個觀察事件點 -b
、-e
、-s
默認關閉,-f
默認打開,當指定觀察點被打開后,在相應事件點會對觀察表達式進行求值並輸出
這里要注意方法入參
和方法出參
的區別,有可能在中間被修改導致前后不一致,除了 -b
事件點 params
代表方法入參外,其余事件都代表方法出參
當使用 -b
時,由於觀察事件點是在方法調用前,此時返回值或異常均不存在
2.3.9、monitor(方法執行監控)
用法
monitor -c <second> <className> <methodName> 定時second秒統計一次指定類指定方法的調用情況
monitor -c <second> -b <className> <methodName> 'params[1] > 1' 方法調用之前統計第二個參數大於1的調用情況
對匹配 class-pattern
/method-pattern
/condition-express
的類、方法的調用進行監控。
monitor
命令是一個非實時返回命令.
實時返回命令是輸入之后立即返回,而非實時返回的命令,則是不斷的等待目標 Java 進程返回信息,直到用戶輸入 Ctrl+C
為止。
服務端是以任務的形式在后台跑任務,植入的代碼隨着任務的中止而不會被執行,所以任務關閉后,不會對原有性能產生太大影響,而且原則上,任何Arthas命令不會引起原有業務邏輯的改變。
monitor和watch的區別是watch是實時監控,每次方法調用都會打印;monitor是統計之后定時打印,watch傾向於實時查看,monitor傾向於定時統計
monitor可以監控的維度包括:timestamp(時間戳)、class(類)、method(方法)、total(調用次數)、success(成功次數)、fail(失敗次數)、rt(平均RT)、fail-rate(失敗率)
monitor結果如下圖示:
2.3.10、trace(方法調用路徑)
用法
trace <className> <methodName> 實時打印指定類指定方法的調用鏈路
trace <className> <methodName> --skipJDKMethod false 打印JDK方法執行鏈路,默認會被過濾
trace <className> <method> '#cost > 100' 過濾耗時大於100毫秒的調用鏈路
trace <className> <method> '#cost > 100' -n <n> 限制實時監控的次數為n次
trace可以查詢指定類的指定方法調用路徑,渲染和統計整個調用鏈路上的所有性能開銷和追蹤調用鏈路
打印信息中包含了整個鏈路的每一步,並且打印了每一個鏈路的代碼行數和耗時情況
2.3.11、stack(輸出當前方法被調用的路徑)
用法
stack <className> <methodName> 打印某個方法被調用的鏈路
stack <className> <methodName> 'params[0] > 1' 按入參條件過濾
stack <className> <methodName> '#cost > 100' 按耗時進行過濾
當一個方法可以被很多地方調用時,想要查詢當前方法是被哪個方法調用,此時就可以通過stack命令得知當前方法是從什么地方被執行的,如下圖示
3、Arthas使用案例
案例一:排查方法執行異常
場景:線上某個接口偶爾會報500錯誤,此時就需要排查報500錯誤時的具體參數
可以通過watch命令監控接口報500錯誤時的參數和異常信息命令如下:
watch <類路徑,支持通配符> <方法名,支持通配符> "{params, throwExp}" -e 表示當發生異常時打印出參數和異常信息內容
案例二:熱更新代碼
場景:線上存在一個小BUG,需要修改部分代碼就可以修復,而發版成本較大時就可以通過熱更新的方式替換線上的代碼邏輯
熱更新涉及到幾個步驟,
1、首先需要將.class文件進行反編譯成源代碼文件
2、然后通過編輯vim編輯源代碼文件更新代碼邏輯
3、然后再將新源代碼文件編譯成.class文件
4、最后再將新的.class文件替換掉JVM中的已加載的.class文件
假設用一個UserController的getUserDetail代碼如下:
1 @RestController 2 @RequestMapping(value = "/testUser") 3 public class TestController { 4 5 @RequestMapping(value = "/detail", method = RequestMethod.GET) 6 public String getUserName(@RequestParam("userId") String userId){ 7 return "name is :" + Long.valueOf(userId); 8 } 9 }
由於參數userId定義的是String類型,因此可能會出現第7行的轉換異常,所以需要將userId類型改成Long類型,並通過熱更新的方式部署
第一步:通過jad命令反編譯UserController類,並將反編譯后的代碼寫入臨時文件夾
jad --source-only com.zjic.message.business.controller.TestController > /tmp/TestController.java
第二步:通過vim來編輯TestController.java文件,修改源代碼將入參的String類型改成Long類型
vim /tmp/TestController
第三步:編譯新的TestController.java文件
編譯UserController可以指定原先的類加載器編譯,那么可以先通過sc命令找到原先的類加載
sc -d *TestController | grep 'classLoader'
查到classLoader的hash碼后就可以通過mc命令編譯新的TestController.java文件,並寫入到tmp目錄生成TestController.class文件
mc -c 17d99928 /tmp/TestController.java -d /tmp
第四步:通過redefine命令將新生成的TestController.class替換掉JVM中已加載的TestController
1 redefine /tmp/com/zjic/message/business/controller/TestController.class
結果如下:
提示刪除了一個方法所以替換失敗,因為通過反編譯修改新的TestController.java文件時將方法的參數類型進行了修改,那么就相當於重新定義了一個方法,所以想要通過redefine替換.class文件時,類中的方法個數、參數個數、參數類型以及返回值都不允許修改,只可以修改方法內部的實現邏輯。
重新修改TestController.java文件,不修改參數userId的類型在方法體內部添加try/catch捕獲異常,
@RestController @RequestMapping(value={"/testUser"}) public class TestController { @RequestMapping(value={"/detail"}, method={RequestMethod.GET}) public String getUserName(@RequestParam(value="userId") String userId) { Long id = null; try{ id = Long.parseLong(userId); }catch(Exception e){ } return "name is :" + id; } }
並重新編譯TestController.class文件,最后重新執行redefine命令,結果如下:
測試接口驗證通過,從而成功實現了熱更新功能
案例三:線上慢請求排查
場景:當訪問線上某個接口時,發現接口相應比較慢並且通過代碼又不太好排查出具體是哪一行代碼引起的慢請求
通過trace命令可以監控接口方法的調用鏈路和各個鏈路的耗時,從而可以分析接口最耗時的代碼塊在哪,案例如下:
trace com.zjic.message.business.controller.BusinessApplyController * '#cost > 1'
先監控指定Controller的所有方法,采用*通配符匹配,然后可以發現最耗時的代碼是Service層的方法,此時就可以再繼續使用trace命令繼續查看Service層方法的調用鏈路
最終定位到最耗時的是Mapper層的方法,所以該接口最耗時的就是SQL語句的執行,那么就可以針對SQL優化來進行接口的優化
4、Arthas實現原理
4.1、JVM的Attach機制
4.1.1、Attach機制實現原理
想要了解Arthas的實現原理,首先需要理解JVM的attach機制,attach機制的主要功能就是實現了一個JVM進程和另一個JVM進程之前相互發送命令進行通信的機制。
JVM運行時的相關狀態數據只有JVM本身掌握,此時想要掌握只能通過訪問JVM來查詢,而attach機制就相當於JVM對其他JVM進程開放了一個對接入口,其他JVM進程只要attach到當前JVM就可以發生命令讓當前JVM執行。
常見的jstack、jmap、debug等操作都是通過attach機制來實現的。
attach機制實現的關鍵是兩個JVM線程,分別是Attach Listener線程和Signal Dipatcher線程
Attach Listener線程負責接收外部發送來的JVM命令,並處理JVM命令返回結果
Signal Dispatcher線程負責將分發到JVM信號,然后將結果返回
Attach Listener線程並非是隨着JVM啟動而啟動的,而是需要顯示在啟動JVM時啟動,或者當第一個JVM命令到來時才啟動;而Signal Dispatcher線程是隨着JVM啟動就是創建啟動
當外部進程Attach目標JVM時,會向目標進程發送sigquit信號,目標進程接收到信號之后廣播給子線程,而只要Signal Dispatcher線程會處理該信號,並會創建Attach Listener線程
Attach Listener線程啟動之后,會創建監聽套接字文件/tmp/.java_pid,表示外部進程Attach目標JVM成功。之后外部進程發送命令寫入該套接字,Attach Listener線程監聽該套接字,解析成功命令進行處理。
4.1.2、Attach機制使用
JDK提供了VirtualMachine類可以用於attach目標JVM,可以直接調用VirtualMachine的attach(pid)方法attach到目標JVM,獲取到目標JVM的虛擬機對象,代碼如下:
/** attach進程ID為1357的JVM進程,獲取虛擬機對象 */ VirtualMachine virtualMachine = VirtualMachine.attach("1357");
通過attach獲取到虛擬機VirtualMachine對象之后,就可以直接通過VirtualMachine提供的API訪問JVM,例如查詢JVM環境參數、內存dump、線程dump等數據,實際就是向JVM發送指定的命令,VirtualMachine的子類為HotSpotVirtualMachine
部分方法如下:
/** 執行datadump命令 */ public void localDataDump() throws IOException { this.executeCommand("datadump").close(); } /** 執行threaddump命令 */ public InputStream remoteDataDump(Object... var1) throws IOException { return this.executeCommand("threaddump", var1); } /** 執行dumpheap命令 */ public InputStream dumpHeap(Object... var1) throws IOException { return this.executeCommand("dumpheap", var1); } /** 執行inspectheap命令 */ public InputStream heapHisto(Object... var1) throws IOException { return this.executeCommand("inspectheap", var1); }
而常用的jstack命令實際就是調用了HotSpotVirtualMachine的remoteDataDump方法實現的,jmap命令實際激素調用了HotSpotVirtualMachine的dumpHeap方法實現的。
4.2、Java agent
Arthas命令中和查詢相關的命令基本上就可以通過Attach機制來實現,但是Arthas命令中還包括了一些操作的命令,比如熱部署相關的命令等,
這些命令可以實現修改JVM中已經加載的class的功能。這些通過Attach機制是無法實現的,此時就可以通過Java agent來實現Java agent也可以叫做Java探針,是一種可以動態修改字節碼的技術。
Java類的運行是需要先將Java類編譯成字節碼,然后將字節碼加載到JVM中運行的,而Java agent實現的功能就是可以動態的修改已經加載到JVM中的字節碼從而實現動態擴展的效果。Java agent可以在JVM加載字節碼之前修改也可以在JVM加載之后修改。
Java agent修改字節碼有兩個時機,一個是在執行main方法之前的premain方法中添加攔截邏輯,當字節碼加載時進行攔截和修改;另一個就是在JVM運行期間修改,此時就需要通過Attach機制來實現,可以調用VirtualMachine的loadAgent方法加載agent邏輯
想要繼續了解Java agent實現原理之前就首先需要了解JVMTI和JVMTIAgent
JVMTI
JVMTI是JVM ToolInterface,直譯就是JVM工具接口,顧名思義就是JVM提供給用戶的一套工具接口,用戶可以通過訪問JVMTI相關接口來訪問JVM。
JVMTI是基於事件驅動的,當JVM觸發了特定的事件時(如加載class)會調用該事件對應的回調函數,而回調函數就可以用來進行功能擴展。
JVMTIAgent
JVMTI是一套本地代碼接口,因此想要使用JVMTI就需要和C/C++打交道,所以就需要有一個代理來負責和JVMTI交互,Java程序只需要和代理交互即可,這個代理就是JVMTIAgent。將代理編譯成動態鏈接庫之后就可以在JVM啟動時加載,也可以在JVM運行期間加載。
JVMTIAgent主要提供了三個方法,分別是Agent_OnLoad、Agent_OnAttach、Agent_OnUpload
Agent_OnLoad:如果agent在啟動時加載就執行此方法;
Agent_OnAttach:如果agent在attach某個JVM進程后發送loadAgent命令時就調用此方法
Agent_OnUpload:當agent被卸載時會調用
Instrument
Instrument就是JDK6提供的一個JVMTIAgent動態鏈接庫,也就是一個.dll,並且實現了Agent_OnLoad和Agent_OnAttach方法,所以Instrument提供了agent在JVM啟動時和JVM運行時兩個時機加載agent的方法
啟動時加載就是在啟動時添加JVM參數:-javaagent:XXXAgent.jar的方式
運行時加載是通過JVM的attach機制來實現,通過發送load命令來加載
工作流程
1、JVM啟動時先加載agent,找到動態鏈接庫dll,其中就包含了Instrument對應的dll
2、執行Instrument的Agent_OnLoad方法
3、Instrument的Agent_OnLoad方法會創建一個Instrumentation接口的實例InstrumentationImpl對象
4、並監聽ClassFileLoadHook事件,也就是類被加載的事件
5、然后調用InstrumentationImpl的loadClassAndCallPremain方法,在這個方法里會去調用javaagent里MANIFEST.MF里指定的Premain-Class類的premain方法
6、premain方法可以獲取到InstrumentationImpl實例,JVM啟動時執行agent的Agent_OnLoad方法,該方法會創建一個Instrumentation接口的實例InstrumentationImpl對象,然后監聽ClassFileLoadHook(類加載事件),
調用InstrumentationImpl類的loadClassAndCallPremain方法,這個方法會調用javaagent的jar包中里的MANIFEST.MF里指定的Premain-Class類的premain方法