這篇文章是以前學習Arthas和jvm-sandbox的一些心得和總結,但願能幫助到你們。本文字較多,能夠根據目錄進行對應的閱讀。html
2018年已過,可是在過去的一年裏面開源了不少優秀的項目,這裏我要介紹兩個比較類似的阿里開源項目一個是Arthas,另外一個是jvm-sandbox。這兩個項目都是在今年開源的,爲何要介紹這兩個項目呢?這裏先賣個關子,先問下你們不知道是否遇到過下面的場景呢?java
以上這些場景,再真正的業務開發中你們或多或少都碰見過,而通常你們的處理方式和我在場景的描述得大致一致。而這裏要給你們介紹一下Arthas和jvm-sandbox,若是你學會了這兩個項目,上面全部的問題再你手上不再是難事。git
固然再介紹Arthas以前仍是要給你們說一下Greys,不管是Arthas仍是jvm-sandbox都是從Greys演變而來,這個是2014年阿里開源的一款Java在線問題診斷工具。而Arthas能夠看作是他的升級版本,是一款更加優秀的,功能更加豐富的Java診斷工具。 在他的github的READEME中的介紹這款工具能夠幫助你作下面這些事:github
下面我將會介紹一下Arthas的一些經常使用的命令和用法,看看是如何解決咱們實際中的問題的,至於安裝教程能夠參考Arthas的github。sql
相信你們都遇到過NoSuchMethodError這個錯誤,通常老司機看見這個錯誤第一反應就是jar包版本號衝突,這種問題通常來講使用maven的一些插件就能輕鬆解決。apache
以前遇到個奇怪的問題,咱們有兩個服務的client-jar包,有個類的包名和類名均是一致,在編寫代碼的時候沒有注意到這個問題,在編譯階段因爲包名和類名都是一致,全部編譯階段並無報錯,在線下的運行階段沒有問題,可是測試環境的機器中的運行階段缺報出了問題。這個和以前的jar包版本號衝突有點不一樣,由於在排查的時候咱們想使用A服務的client-jar包的這個類,可是這個jar包的版本號在Maven中的確是惟一的。數組
這個時候Arthas就能夠大顯神通了。oracle
找到對應的類,而後輸出下面的命令(用例使用的是官方提供的用例):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來進行獲取的:
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包來進行反編譯。這裏過程比較複雜這裏就不進行敘述。
有不少同窗可能會以爲動態修改日誌有什麼用呢?好像本身也沒怎麼用過呢? 通常來講下面這幾個場景能夠須要:
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中獲取中去獲取。
不少時候咱們方法執行的狀況和咱們預期不符合,可是咱們又不知道到底哪裏不符合,Arthas的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 記錄方法的入參出參,以及方法消耗時間。
當某個方法耗時較長,這個時候你須要排查究竟是某一處發生了長時間的耗時,通常這種問題比較難排查,都是經過全鏈路追蹤trace圖去進行排查,可是在本地的應用中沒有trace圖,這個時候須要Arthas的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。在訪問方法以前和以後會進行記錄。
有時候排查一個問題須要上游再次調用這個方法,好比使用postMan等工具,固然Arthas提供了一個命令讓替代咱們來回手動請求。
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信息可能會丟失
引用的對象:保存的入參是保存的引用,而不是拷貝,因此若是參數中的內容被修改,那麼入參其實也是被修改的。
有時候有些方法很是耗時或者很是重要,須要知道究竟是誰發起的調用,好比System.gc(),有時候若是你發現fullgc頻繁是由於System.gc()引發的,你須要查看究竟是什麼應用調用的,那麼你就可使用下面的命令。
咱們能夠輸入下面的命令:
$ 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進行進行監視,而後記錄當前的堆棧來獲取是什麼位置進行的調用。
有些時候咱們找了全部的命令,發現和咱們的需求並不符合的時候,那麼這個時候咱們能夠從新定義這個類,咱們能夠用使用下面的命令。
redefine命令提供了咱們能夠從新定義jvm中的class,可是使用這個命令以後class不可恢復。咱們首先須要把重寫的class編譯出來,而後上傳到咱們指定的目錄,進行下面的操做:
redefine -p /tmp/Test.class 複製代碼
能夠重定義咱們的Test.class。從而修改邏輯,完成咱們自定義的需求。
上面介紹了7種Arthas比較常見的場景和命令。固然這個命令還遠遠不止這麼點,每一個命令的用法也沒有侷限於我介紹的。尤爲是開源之後更多的開發者參與了進來,如今也將其優化成能夠有界面的,在線排查問題的方式,來解決去線上安裝的各類不便。
更多的命令能夠參考Arthas的用戶文檔:alibaba.github.io/arthas/inde…
上面已經給你們介紹了強大的Arthas,有不少人也想作一個能夠動態替換Class的工具,可是這種東西過於底層,比較小衆,入門的門檻相對來講比較高。可是jvm-sandbox,給咱們提供了用通俗易懂的編碼方式來動態替換Class。
對於AOP來講你們確定對其不陌生,在Spring中咱們能夠很方便的實現一個AOP,可是這樣有兩個缺點:一個是隻能針對Spring中的Bean進行加強,還有個是加強以後若是要修改加強內容那麼就只能重寫而後發佈項目,不能動態的加強。
JVM Sandbox 利用 HotSwap 技術在不重啓 JVM的狀況下實現:
也就是咱們能夠經過這種技術來完成咱們在arthas的命令。 通常來講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是咱們的核心方法。流程以下:
Arthas是一款很優秀的Java線上問題診斷工具,Sandbox的做者沒有選擇和Arthas去作一個功能很全的工具平臺,而選擇了去作一款底層中臺,讓更多的人能夠很輕鬆的去實現字節碼加強相關的工具。若是說Arthas是一把鋒利的劍能斬殺萬千敵人,那麼jvm-sandbox就是打造一把好劍的模子,等待着你們去打造一把屬於本身的絕世好劍。
sadbox介紹得比較少,有興趣的同窗能夠去github上自行了解:github.com/alibaba/jvm…
不論上咱們的Arthas仍是咱們的jvm-sandbox無外乎使用的就是下面幾種技術:
對於ASM字節碼修改技術能夠參考我以前寫的幾篇文章:
對於ASM修改字節碼的技術這裏就不作多餘闡述。
Instrumentation是JDK1.6用來構建Java代碼的類。Instrumentation是在方法中添加字節碼來達到收集數據或者改變流程的目的。固然他也提供了一些額外功能,好比獲取當前JVM中全部加載的Class等。
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的接口
本文實現的HotSwap的代碼均在https://github.com/lzggsimida123/hotswapsample中,下面簡單介紹一下:
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打印出來。
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…
A:執行順序以下:
在同一級當中,按照添加順序進行處理。
A:redefineClass的class不可找回到之前的,不會觸發咱們的Transformer,retransClass會根據當前的calss而後依次執行Transformer作class替換。
A:在jdk文檔中的解釋是,不會影響當前調用,會在本次調用結束之後纔會加載咱們替換的class。
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可以很好的幫助你。同時瞭解他們的底層原理也能對你在調優或者排查問題的時候起很大的幫助做用。字數有點多,但願你們能學習到有用的知識。
參考文檔: