解密阿里線上問題診斷工具Arthas和jvm-sandbox

咖啡拿鐵發表於2019-01-22

大綱目錄

這篇文章是之前學習Arthas和jvm-sandbox的一些心得和總結,希望能幫助到大家。本文字較多,可以根據目錄進行對應的閱讀。

  • 背景:現在的問題所在?
  • Arthas: Arthas能幫助你幹什麼?各種命令原理是什麼?
  • jvm-sandbox: jvm-sandbox能幫助你幹什麼?
  • 實現原理?自己如何實現一個?
  • 常見的一些問題?

1.背景

2018年已過,但是在過去的一年裡面開源了很多優秀的專案,這裡我要介紹兩個比較相似的阿里開源專案一個是Arthas,另一個是jvm-sandbox。這兩個專案都是在今年開源的,為什麼要介紹這兩個專案呢?這裡先賣個關子,先問下大家不知道是否遇到過下面的場景呢?

  • 當你線上專案出了問題,但是一開啟日誌發現,有些地方忘記打了日誌,於是你馬上補上日誌,然後重新上線。這個在一些上線流程不規範的公司還比較輕鬆,在一些流程比較嚴格,比如美團上線的時候就有封禁期,一般就只能9點之後才能上線。有可能這樣一拖就耽誤瞭解決問題的黃金時刻。
  • 當你的專案某個介面執行速度較慢,為了排查問題,於是你四處加上每個方法執行時間。
  • 當你發現某個類有衝突,好像線上上執行的結果和你預期的不符合,手動把線上編譯出的class檔案下載下來然後反編譯,看看究竟class內容是什麼。
  • 當程式碼已經寫好準備聯調,但是下游業務環境並沒有準備好,於是你把以前的程式碼依次進行註釋,採用mock的形式又寫了一遍方便聯調。

以上這些場景,再真正的業務開發中大家或多或少都遇見過,而一般大家的處理方式和我在場景的描述得大體一致。而這裡要給大家介紹一下Arthas和jvm-sandbox,如果你學會了這兩個專案,上面所有的問題再你手上再也不是難事。

2. Arthas

當然再介紹Arthas之前還是要給大家說一下Greys,無論是Arthas還是jvm-sandbox都是從Greys演變而來,這個是2014年阿里開源的一款Java線上問題診斷工具。而Arthas可以看做是他的升級版本,是一款更加優秀的,功能更加豐富的Java診斷工具。 在他的github的READEME中的介紹這款工具可以幫助你做下面這些事:

  • 這個類從哪個 jar 包載入的?為什麼會報各種類相關的 Exception?
  • 我改的程式碼為什麼沒有執行到?難道是我沒 commit?分支搞錯了?
  • 遇到問題無法線上上 debug,難道只能通過加日誌再重新發布嗎?
  • 線上遇到某個使用者的資料處理有問題,但線上同樣無法 debug,線下無法重現!
  • 是否有一個全域性視角來檢視系統的執行狀況?
  • 有什麼辦法可以監控到JVM的實時執行狀態?

下面我將會介紹一下Arthas的一些常用的命令和用法,看看是如何解決我們實際中的問題的,至於安裝教程可以參考Arthas的github。

2.1 奇怪的類載入錯誤

相信大家都遇到過NoSuchMethodError這個錯誤,一般老司機看見這個錯誤第一反應就是jar包版本號衝突,這種問題一般來說使用maven的一些外掛就能輕鬆解決。

之前遇到個奇怪的問題,我們有兩個服務的client-jar包,有個類的包名和類名均是一致,在編寫程式碼的時候沒有注意到這個問題,在編譯階段由於包名和類名都是一致,所有編譯階段並沒有報錯,線上下的執行階段沒有問題,但是測試環境的機器中的執行階段缺報出了問題。這個和之前的jar包版本號衝突有點不同,因為在排查的時候我們想使用A服務的client-jar包的這個類,但是這個jar包的版本號在Maven中的確是唯一的。

這個時候Arthas就可以大顯神通了。

2.1.1 sc命令

找到對應的類,然後輸出下面的命令(用例使用的是官方提供的用例):

$ sc -d demo.MathGame
class-info        demo.MathGame
code-source       /private/tmp/arthas-demo.jar
name              demo.MathGame
isInterface       false
isAnnotation      false
isEnum            false
isAnonymousClass  false
isArray           false
isLocalClass      false
isMemberClass     false
isPrimitive       false
isSynthetic       false
simple-name       MathGame
modifier          public
annotation
interfaces
super-class       +-java.lang.Object
class-loader      +-sun.misc.Launcher$AppClassLoader@3d4eac69
                    +-sun.misc.Launcher$ExtClassLoader@66350f69
classLoaderHash   3d4eac69
 
Affect(row-cnt:1) cost in 875 ms.
複製程式碼

可以看見列印出了code-source,當時發現了code-source並不是從對應的Jar包取出來的,於是發現了兩個服務對於同一個類使用了同樣的包名和類名,導致了這個奇怪的問題,後續通過修改包名和類名進行解決。

sc原理

sc的資訊主要從對應的Class中獲取。 比如isInterface,isAnnotation等等都是通過下面的方式獲取:

解密阿里線上問題診斷工具Arthas和jvm-sandbox
對於我們上面的某個類從哪個jar包載入的是通過CodeSource來進行獲取的:

解密阿里線上問題診斷工具Arthas和jvm-sandbox

2.1.2 jad

Arthas還提供了一個命令jad用來反編譯,對於解決類衝突錯誤很有用,比如我們想知道這個類裡面的程式碼到底是什麼,直接一個jad命令就能搞定:

$ jad java.lang.String
 
ClassLoader:
 
Location:
 
/*
* Decompiled with CFR 0_132.
*/
package java.lang;
 
import java.io.ObjectStreamField;
...
public final class String
implements Serializable,
Comparable<String>,
CharSequence {
    private final char[] value;
    private int hash;
    private static final long serialVersionUID = -6849794470754667710L;
    private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];
    public static final Comparator<String> CASE_INSENSITIVE_ORDER = new CaseInsensitiveComparator();
 
    public String(byte[] arrby, int n, int n2) {
        String.checkBounds(arrby, n, n2);
        this.value = StringCoding.decode(arrby, n, n2);
    }
...

複製程式碼

一般通過這個命令我們就能發現和你所期待的類是否缺少了某些方法,或者某些方法有些改變,從而確定jar包衝突。

jad原理

jad使用的是cfr提供的jar包來進行反編譯。這裡過程比較複雜這裡就不進行敘述。

2.2 動態修改日誌級別

有很多同學可能會覺得動態修改日誌有什麼用呢?好像自己也沒怎麼用過呢? 一般來說下面這幾個場景可以需要:

  • 一般大家日誌級別預設是info,有時候需要檢視debug的日誌可能需要重新上線。
  • 當線上某個應用流量比較大的時候,如何業務出現問題,可能會短時間之內產生大量日誌,由於日誌會寫盤,會消耗大量的記憶體和磁碟IO進一步加重我們的問題嚴重性,進而引起雪崩。 我們可以使用動態修改日誌解決我們上面兩個問題,在美團內部開發了一個工具通過LogContext,記錄下所有的logConfig然後動態修改更新。但是如果沒有這個工具我們如何動態修改日誌呢?

2.2.1 ognl

ognl是一門表示式語言,在Arthas中你可以利用這個表示式語言做很多事,比如執行某個方法,獲取某個資訊。再這裡我們可以通過下面的命令來動態的修改日誌級別:

 $ ognl '@com.lz.test@LOGGER.logger.privateConfig'
@PrivateConfig[
    loggerConfig=@LoggerConfig[root],
    loggerConfigLevel=@Level[INFO],
    intLevel=@Integer[400],
]
$ ognl '@com.lz.test@LOGGER.logger.setLevel(@org.apache.logging.log4j.Level@ERROR)'
null
$ ognl '@com.lz.test@LOGGER.logger.privateConfig'
@PrivateConfig[
    loggerConfig=@LoggerConfig[root],
    loggerConfigLevel=@Level[ERROR],
    intLevel=@Integer[200],
  
]

複製程式碼

上面的命令可以修改對應類中的info日誌為error日誌列印級別,如果想全域性修改root的級別的話對於ognl表示式來說執行比較困難,總的來說需要將ognl翻譯為下面這段程式碼:

org.apache.logging.log4j.core.LoggerContext loggerContext = (org.apache.logging.log4j.core.LoggerContext) org.apache.logging.log4j.LogManager.getContext(false);
        Map<String, LoggerConfig> map = loggerContext.getConfiguration().getLoggers();
        for (org.apache.logging.log4j.core.config.LoggerConfig loggerConfig : map.values()) {
            String key = loggerConfig.getName();
            if (StringUtils.isBlank(key)) {
                loggerConfig.setLevel(Level.ERROR);
            }
        }
loggerContext.updateLoggers();
複製程式碼

總的來說比較複雜,這裡不給予實現,如果有興趣的可以用程式碼的形式去實現以下,美團的動態調整日誌元件也是通過這種方法實現的。

原理

具體原理是首先獲取AppClassLoader(預設)或者指定的ClassLoader,然後再呼叫Ognl的包,自動執行解析這個表示式,而這個執行的類都會從前面的ClassLoader中獲取中去獲取。

2.3 如何知道某個方法是否呼叫

很多時候我們方法執行的情況和我們預期不符合,但是我們又不知道到底哪裡不符合,Arthas的watch命令就能幫助我們解決這個問題。

2.3.1 watch

watch命令顧名思義觀察,他可以觀察指定方法呼叫情況,定義了4個觀察事件點, -b 方法呼叫前,-e 方法異常後,-s 方法返回後,-f 方法結束後。預設是-f

比如我們想知道某個方法執行的時候,引數和返回值到底是什麼。注意這裡的引數是方法執行完成的時候的引數,和入參不同有可能會發生變化。

$ watch demo.MathGame primeFactors "{params,returnObj}" -x 2
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 44 ms.
ts=2018-12-03 19:16:51; [cost=1.280502ms] result=@ArrayList[
    @Object[][
        @Integer[535629513],
    ],
    @ArrayList[
        @Integer[3],
        @Integer[19],
        @Integer[191],
        @Integer[49199],
    ],
]
複製程式碼

你能得到引數和返回值的情況,以及方法時間消耗的等資訊。

原理

利用jdk1.6的instrument + ASM 記錄方法的入參出參,以及方法消耗時間。

2.4 如何知道某個方法耗時較多

當某個方法耗時較長,這個時候你需要排查到底是某一處發生了長時間的耗時,一般這種問題比較難排查,都是通過全鏈路追蹤trace圖去進行排查,但是在本地的應用中沒有trace圖,這個時候需要Arthas的trace命令來進行排查問題。

2.4.1 trace

trace 命令能主動搜尋 class-pattern/method-pattern 對應的方法呼叫路徑,渲染和統計整個呼叫鏈路上的所有效能開銷和追蹤呼叫鏈路。

但是trace只能追蹤一層的呼叫鏈路,如果一層的鏈路資訊不夠用,可以把該鏈路上有問題的方法再次進行trace。 trace使用例子如下。

$ trace demo.MathGame run
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 42 ms.
`---ts=2018-12-04 00:44:17;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69
    `---[10.611029ms] demo.MathGame:run()
        +---[0.05638ms] java.util.Random:nextInt()
        +---[10.036885ms] demo.MathGame:primeFactors()
        `---[0.170316ms] demo.MathGame:print()
複製程式碼

可以看見上述耗時最多的方法是primeFactors,所以我們可以對其進行trace進行再一步的排查。

原理

利用jdk1.6的instrument + ASM。在訪問方法之前和之後會進行記錄。

2.5 如何使用命令重發請求?

有時候排查一個問題需要上游再次呼叫這個方法,比如使用postMan等工具,當然Arthas提供了一個命令讓替代我們來回手動請求。

2.5.1 tt

tt官方介紹: 方法執行資料的時空隧道,記錄下指定方法每次呼叫的入參和返回資訊,並能對這些不同的時間下呼叫進行觀測。可以看見tt可以用於錄製請求,當然也支援我們重放。 如果要錄製某個方法,可以用下面命令:

$ tt -t demo.MathGame primeFactors
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 66 ms.
 INDEX   TIMESTAMP            COST(ms)  IS-RET  IS-EXP   OBJECT         CLASS                          METHOD
-------------------------------------------------------------------------------------------------------------------------------------
 1000    2018-12-04 11:15:38  1.096236  false   true     0x4b67cf4d     MathGame                       primeFactors
 1001    2018-12-04 11:15:39  0.191848  false   true     0x4b67cf4d     MathGame                       primeFactors
 1002    2018-12-04 11:15:40  0.069523  false   true     0x4b67cf4d     MathGame                       primeFactors
 1003    2018-12-04 11:15:41  0.186073  false   true     0x4b67cf4d     MathGame                       primeFactors
 1004    2018-12-04 11:15:42  17.76437  true    false    0x4b67cf4d     MathGame                       primeFactors

複製程式碼

上面錄製了5個呼叫環境現場,也可以看做是錄製了5個請求返回資訊。比如我們想選擇index為1004個的請求來重放,可以輸入下面的命令。

$ tt -i 1004 -p
 RE-INDEX       1004
 GMT-REPLAY     2018-12-04 11:26:00
 OBJECT         0x4b67cf4d
 CLASS          demo.MathGame
 METHOD         primeFactors
 PARAMETERS[0]  @Integer[946738738]
 IS-RETURN      true
 IS-EXCEPTION   false
 RETURN-OBJ     @ArrayList[
                    @Integer[2],
                    @Integer[11],
                    @Integer[17],
                    @Integer[2531387],
                ]
Time fragment[1004] successfully replayed.
Affect(row-cnt:1) cost in 14 ms.
複製程式碼

注意重放請求需要關注兩點:

  • ThreadLocal 資訊丟失:由於使用的是Arthas執行緒呼叫,會讓threadLocal資訊丟失,比如一些TraceId資訊可能會丟失

  • 引用的物件:儲存的入參是儲存的引用,而不是拷貝,所以如果引數中的內容被修改,那麼入參其實也是被修改的。

2.6 一些耗時的方法,經常被觸發,如何知道誰呼叫的?

有時候有些方法非常耗時或者非常重要,需要知道到底是誰發起的呼叫,比如System.gc(),有時候如果你發現fullgc頻繁是因為System.gc()引起的,你需要檢視到底是什麼應用呼叫的,那麼你就可以使用下面的命令。

2.6.1

我們可以輸入下面的命令:

$ options unsafe true
 NAME    BEFORE-VALUE  AFTER-VALUE                                                                                                                                                                        
-----------------------------------                                                                                                                                                                       
 unsafe  false         true                                                                                                                                                                               
$ stack java.lang.System gc
Press Ctrl+C to abort.
Affect(class-cnt:1 , method-cnt:1) cost in 50 ms.
ts=2019-01-20 21:14:05;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@14dad5dc
    @java.lang.System.gc()
        at com.lz.test.Test.main(Test.java:322)

複製程式碼

首先輸入options unsafe true允許我們對jdk增強,然後對System.gc進行進行監視,然後記錄當前的堆疊來獲取是什麼位置進行的呼叫。

2.7 如何重定義某個類?

有些時候我們找了所有的命令,發現和我們的需求並不符合的時候,那麼這個時候我們可以重新定義這個類,我們可以用使用下面的命令。

2.7.1 redefine

redefine命令提供了我們可以重新定義jvm中的class,但是使用這個命令之後class不可恢復。我們首先需要把重寫的class編譯出來,然後上傳到我們指定的目錄,進行下面的操作:

 redefine -p /tmp/Test.class
複製程式碼

可以重定義我們的Test.class。從而修改邏輯,完成我們自定義的需求。

2.8 Arthas小結

上面介紹了7種Arthas比較常見的場景和命令。當然這個命令還遠遠不止這麼點,每個命令的用法也沒有侷限於我介紹的。尤其是開源以後更多的開發者參與了進來,現在也將其優化成可以有介面的,線上排查問題的方式,來解決去線上安裝的各種不便。

更多的命令可以參考Arthas的使用者文件:alibaba.github.io/arthas/inde…

3.jvm-sandbox

上面已經給大家介紹了強大的Arthas,有很多人也想做一個可以動態替換Class的工具,但是這種東西過於底層,比較小眾,入門的門檻相對來說比較高。但是jvm-sandbox,給我們提供了用通俗易懂的編碼方式來動態替換Class。

3.1 AOP

對於AOP來說大家肯定對其不陌生,在Spring中我們可以很方便的實現一個AOP,但是這樣有兩個缺點:一個是隻能針對Spring中的Bean進行增強,還有個是增強之後如果要修改增強內容那麼就只能重寫然後釋出專案,不能動態的增強。

3.2 sanbox能帶來什麼

JVM Sandbox 利用 HotSwap 技術在不重啟 JVM的情況下實現:

  • 在執行期完成對 JVM 中任意類裡的任意方法的 AOP 增強
  • 可以動態熱插拔擴充套件模組
  • 通過擴充套件模組改變任意方法執行的流程

也就是我們可以通過這種技術來完成我們在arthas的命令。 一般來說sandbox的適用場景如下:

  • 線上故障定位
  • 線上系統流控
  • 線上故障模擬
  • 方法請求錄製和結果回放
  • 動態日誌列印
  • ...

當然還有更多的場景,他能做什麼完全取決於你的想象,只要你想得出來他就能做到。

3.3 sandbox例子

sandbox提供了Module的概念,每個Module都是一個AOP的例項。 比如我們想完成一個列印所有jdbc statement sql日誌的Module,需要建一個下面的Module:

public class JdbcLoggerModule implements Module, LoadCompleted {

    private final Logger smLogger = LoggerFactory.getLogger("DEBUG-JDBC-LOGGER");

    @Resource
    private ModuleEventWatcher moduleEventWatcher;

    @Override
    public void loadCompleted() {
        monitorJavaSqlStatement();
    }

    // 監控java.sql.Statement的所有實現類
    private void monitorJavaSqlStatement() {
        new EventWatchBuilder(moduleEventWatcher)
                .onClass(Statement.class).includeSubClasses()
                .onBehavior("execute*")
                /**/.withParameterTypes(String.class)
                /**/.withParameterTypes(String.class, int.class)
                /**/.withParameterTypes(String.class, int[].class)
                /**/.withParameterTypes(String.class, String[].class)
                .onWatch(new AdviceListener() {

                    private final String MARK_STATEMENT_EXECUTE = "MARK_STATEMENT_EXECUTE";
                    private final String PREFIX = "STMT";

                    @Override
                    public void before(Advice advice) {
                        advice.attach(System.currentTimeMillis(), MARK_STATEMENT_EXECUTE);
                    }

                    @Override
                    public void afterReturning(Advice advice) {
                        if (advice.hasMark(MARK_STATEMENT_EXECUTE)) {
                            final long costMs = System.currentTimeMillis() - (Long) advice.attachment();
                            final String sql = advice.getParameterArray()[0].toString();
                            logSql(PREFIX, sql, costMs, true, null);
                        }
                    }
                    ....
                });
    }

}
複製程式碼

monitorJavaSqlStatement是我們的核心方法。流程如下:

  1. 首先通過new EventWatchBuilder(moduleEventWatcher)構造一個事件觀察者的構造器,通過Builder我們可以方便的構造出我們的觀察者。
  2. onclass是對我們需要觀察的類進行篩選,includeSubClasses包含所有的子類。
  3. withParameterTypes進一步篩選引數。
  4. onWatch進行觀察,採取模板模式,和我們Spring的AOP很類似,首先在before裡面記錄下當前的時間,然後在afterReturning中將before的時間取出來得到當前消耗的時間,然後獲取當前的sql語句,最後進行列印。

3.4 sandbox小結

Arthas是一款很優秀的Java線上問題診斷工具,Sandbox的作者沒有選擇和Arthas去做一個功能很全的工具平臺,而選擇了去做一款底層中臺,讓更多的人可以很輕鬆的去實現位元組碼增強相關的工具。如果說Arthas是一把鋒利的劍能斬殺萬千敵人,那麼jvm-sandbox就是打造一把好劍的模子,等待著大家去打造一把屬於自己的絕世好劍。

sadbox介紹得比較少,有興趣的同學可以去github上自行了解:github.com/alibaba/jvm…

4.自己實現位元組碼動態替換

不論上我們的Arthas還是我們的jvm-sandbox無外乎使用的就是下面幾種技術:

  • ASM
  • Instrumentation(核心)
  • VirtualMachine

4.1 ASM

對於ASM位元組碼修改技術可以參考我之前寫的幾篇文章:

對於ASM修改位元組碼的技術這裡就不做多餘闡述。

4.2 Instrumentation

Instrumentation是JDK1.6用來構建Java程式碼的類。Instrumentation是在方法中新增位元組碼來達到收集資料或者改變流程的目的。當然他也提供了一些額外功能,比如獲取當前JVM中所有載入的Class等。

4.2.1獲取Instrumentation

Java提供了兩種方法獲取Instrumentation,下面介紹一下這兩種:

4.2.1.1 premain

在啟動的時候,會呼叫preMain方法:

public static void premain(String agentArgs, Instrumentation inst) {
    }

複製程式碼

需要在啟動時新增額外命令

java -javaagent:jar 檔案的位置 [= 傳入 premain 的引數 ] 

複製程式碼

也需要在maven中配置PreMainClass。

教你用Java位元組碼做日誌脫敏工具中很詳細的介紹了premain

4.2.1.2 agentmain

premain是Java SE5開始就提供的代理方式,給了開發者諸多驚喜,不過也有些須不變,由於其必須在命令列指定代理jar,並且代理類必須在main方法前啟動。因此,要求開發者在應用前就必須確認代理的處理邏輯和引數內容等等,在有些場合下,這是比較困難的。比如正常的生產環境下,一般不會開啟代理功能,所有java SE6之後提供了agentmain,用於我們動態的進行修改,而不需要在設定代理。在 JavaSE6文件當中,開發者也許無法在 java.lang.instrument包相關的文件部分看到明確的介紹,更加無法看到具體的應用 agnetmain 的例子。不過,在 Java SE 6 的新特性裡面,有一個不太起眼的地方,揭示了 agentmain 的用法。這就是 Java SE 6 當中提供的 Attach API。

Attach API 不是Java的標準API,而是Sun公司提供的一套擴充套件 API,用來向目標JVM”附著”(Attach)代理工具程式的。有了它,開發者可以方便的監控一個JVM,執行一個外加的代理程式。

在VirtualMachine中提供了attach的介面

4.3 實現HotSwap

本文實現的HotSwap的程式碼均在https://github.com/lzggsimida123/hotswapsample中,下面簡單介紹一下:

4.3.1 redefineClasses

redefineClasses允許我們重新替換JVM中的類,我們現在利用它實現一個簡單的需求,我們有下面一個類:

public class Test1 implements T1 {

    public void sayHello(){
        System.out.println("Test1");
    }
}
複製程式碼

在sayHello中列印Test1,然後我們在main方法中迴圈呼叫sayHello:

 public static void main(String[] args) throws Exception {
        Test1 tt = new Test1();
        int max = 20;
        int index = 0;
        while (++index<max){
            Thread.sleep(100L);
        }
    }
複製程式碼

如果我們不做任何處理,那麼肯定列印出20次Test1。如果我們想完成一個需求,這20次列印是交替列印出Test1,Test2,Test3。那麼我們可以藉助redefineClass。

        //獲取Test1,Test2,Test3的位元組碼
        List<byte[]> bytess = getBytesList();
        int index = 0;
        for (Class<?> clazz : inst.getAllLoadedClasses()) {
            if (clazz.getName().equals("Test1")) {
                while (true) {                
                    //根據index獲取本次對應的位元組碼
                    ClassDefinition classDefinition = new ClassDefinition(clazz, getIndexBytes(index, bytess));
                    // redefindeClass Test1
                    inst.redefineClasses(classDefinition);
                    Thread.sleep(100L);
                    index++;
                }
            }
        }
複製程式碼

可以看見我們獲取了三個calss的位元組碼,在我們根目錄下面有,然後呼叫redefineClasses替換我們對應的位元組碼,可以看見我們的結果,將Test1,Test2,Test3列印出來。

解密阿里線上問題診斷工具Arthas和jvm-sandbox

4.3.2 retransformClasses

redefineClasses直接將位元組碼做了交換,導致原始位元組碼丟失,侷限較大。使用retransformClasses配合我們的Transformer進行轉換位元組碼。同樣的我們有下面這個類:

public class TestTransformer {

    public void testTrans() {
        System.out.println("testTrans1");
    }
}
複製程式碼

在testTrans中列印testTrans1,我們有下面一個main方法:

 public static void main(String[] args) throws Exception {
        TestTransformer testTransformer = new TestTransformer();
        int max = 20;
        int index = 0;
        while (++index<max){
            testTransformer.testTrans();
            Thread.sleep(100L);
        }
複製程式碼

如果我們不做任何操作,那麼肯定列印的是testTrans1,接下來我們使用retransformClasses:

        while (true) {
            try {
                for(Class<?> clazz : inst.getAllLoadedClasses()){
                    if (clazz.getName().equals("TestTransformer")) {
                        inst.retransformClasses(clazz);
                    }
                }
                Thread.sleep(100L);
            }catch (Exception e){
                e.printStackTrace();
            }
        }
複製程式碼

這裡只是將我們對應的類嘗試去retransform,但是需要Transformer:

//必須設定true,才能進行多次retrans
        inst.addTransformer(new SampleTransformer(), true);
複製程式碼

上面新增了一個Transformer,如果設定為false,這下次retransform一個類的時候他不會執行,而是直接返回他已經執行完之後的程式碼。如果設定為true,那麼只要有retransform的呼叫就會執行。

public class SampleTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (!"TestTransformer".equals(className)){
            //返回Null代表不進行處理
            return null;
        }
        //進行隨機輸出testTrans + random.nextInt(3)
        ClassReader reader = new ClassReader(classfileBuffer);
        ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        ClassVisitor classVisitor = new SampleClassVistor(Opcodes.ASM5,classWriter);
        reader.accept(classVisitor,ClassReader.SKIP_DEBUG);
        return classWriter.toByteArray();
        }
    }
}
複製程式碼

這裡的SampleTransFormer使用ASM去對程式碼進行替換,進行隨機輸出testTrans + random.nextInt(3)。可以看有下面的結果:

解密阿里線上問題診斷工具Arthas和jvm-sandbox

上面的程式碼已經上傳至github:github.com/lzggsimida1…

5.常見的一些問題

  1. Q: instrumentation中trans的順序?我有多個Transformer執行順序是怎麼樣的?

A:執行順序如下:

  • 執行不可retransformClasses的Transformer
  • 執行不可retransformClasses的native-Transformer
  • 執行可以retransformClasses的Transformer
  • 執行可以retransformClasses的native-Transformer

在同一級當中,按照新增順序進行處理。

  1. Q: redefineClass和retransClass區別?

A:redefineClass的class不可找回到以前的,不會觸發我們的Transformer,retransClass會根據當前的calss然後依次執行Transformer做class替換。

  1. Q:什麼時候替換?會影響我執行的程式碼嗎?

A:在jdk文件中的解釋是,不會影響當前呼叫,會在本次呼叫結束以後才會載入我們替換的class。

  1. 重新轉換類能改哪些地方?

A: 重新轉換可以會更改方法體、常量池和屬性。重新轉換不能新增、刪除或重新命名欄位或方法、更改方法的簽名或更改繼承。未來版本會取消(java8沒有取消) 5. 哪些類位元組碼不能轉換?

A:私有類,比如Integer.TYPE,和陣列class。

6.JIT的程式碼怎麼辦?

A:清除原來JIT程式碼,然後重新走解釋執行的過程。

7.arthas和jvm-sandbox效能影響?

A:由於新增了部分邏輯,肯定會有影響,並且替換程式碼的時候需要到SafePoint的時候才能替換,進行STW,如果替換程式碼過於頻繁,那麼會頻繁執行STW,這個時候會影響效能。

總結

今年阿里開源的arthas和jvm-sandbox推動了Java線上診斷工具的發展。大家以後遇到一些難以解決的線上問題,那麼arthas肯定是你的首選目標工具之一。當然如果你想要做自己的一些日誌收集,Mock平臺,故障模擬等公共的元件,jvm-sandbox能夠很好的幫助你。同時瞭解他們的底層原理也能對你在調優或者排查問題的時候起很大的幫助作用。字數有點多,希望大家能學習到有用的知識。

參考文件:

最後這篇文章被我收錄於JGrowing-CaseStudy篇,一個全面,優秀,由社群一起共建的Java學習路線,如果您想參與開源專案的維護,可以一起共建,github地址為:github.com/javagrowing… 麻煩給個小星星喲。

如果大家覺得這篇文章對你有幫助,你的關注和轉發是對我最大的支援,O(∩_∩)O:

解密阿里線上問題診斷工具Arthas和jvm-sandbox

相關文章