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

大綱目錄

這篇文章是以前學習Arthas和jvm-sandbox的一些心得和總結,但願能幫助到你們。本文字較多,能夠根據目錄進行對應的閱讀。html

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

1.背景

2018年已過,可是在過去的一年裏面開源了不少優秀的項目,這裏我要介紹兩個比較類似的阿里開源項目一個是Arthas,另外一個是jvm-sandbox。這兩個項目都是在今年開源的,爲何要介紹這兩個項目呢?這裏先賣個關子,先問下你們不知道是否遇到過下面的場景呢?java

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

以上這些場景,再真正的業務開發中你們或多或少都碰見過,而通常你們的處理方式和我在場景的描述得大致一致。而這裏要給你們介紹一下Arthas和jvm-sandbox,若是你學會了這兩個項目,上面全部的問題再你手上不再是難事。git

2. Arthas

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

  • 這個類從哪一個 jar 包加載的?爲何會報各類類相關的 Exception?
  • 我改的代碼爲何沒有執行到?難道是我沒 commit?分支搞錯了?
  • 遇到問題沒法在線上 debug,難道只能經過加日誌再從新發布嗎?
  • 線上遇到某個用戶的數據處理有問題,但線上一樣沒法 debug,線下沒法重現!
  • 是否有一個全局視角來查看系統的運行情況?
  • 有什麼辦法能夠監控到JVM的實時運行狀態?

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

2.1 奇怪的類加載錯誤

相信你們都遇到過NoSuchMethodError這個錯誤,通常老司機看見這個錯誤第一反應就是jar包版本號衝突,這種問題通常來講使用maven的一些插件就能輕鬆解決。apache

以前遇到個奇怪的問題,咱們有兩個服務的client-jar包,有個類的包名和類名均是一致,在編寫代碼的時候沒有注意到這個問題,在編譯階段因爲包名和類名都是一致,全部編譯階段並無報錯,在線下的運行階段沒有問題,可是測試環境的機器中的運行階段缺報出了問題。這個和以前的jar包版本號衝突有點不一樣,由於在排查的時候咱們想使用A服務的client-jar包的這個類,可是這個jar包的版本號在Maven中的確是惟一的。數組

這個時候Arthas就能夠大顯神通了。oracle

2.1.1 sc命令

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

$ 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包取出來的,因而發現了兩個服務對於同一個類使用了一樣的包名和類名,致使了這個奇怪的問題,後續經過修改包名和類名進行解決。jvm

sc原理

sc的信息主要從對應的Class中獲取。 好比isInterface,isAnnotation等等都是經過下面的方式獲取:

 

對於咱們上面的某個類從哪一個jar包加載的是經過CodeSource來進行獲取的:

 

 

 

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打印出來。

 

 

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)。能夠看有下面的結果:

 

 

上面的代碼已經上傳至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可以很好的幫助你。同時瞭解他們的底層原理也能對你在調優或者排查問題的時候起很大的幫助做用。字數有點多,但願你們能學習到有用的知識。

參考文檔:

相關文章
相關標籤/搜索