厲害了!不重啟JVM,替換掉已經(jīng)加載的類
在遙遠的希艾斯星球爪哇國塞沃城中,兩名年輕的程序員正在為一件事情苦惱,程序出問題了,一時看不出問題出在哪里,于是有了以下對話:
“Debug一下吧?!?/p>
“線上機器,沒開Debug端口?!?/p>
“看日志,看看請求值和返回值分別是什么?”
“那段代碼沒打印日志?!?/p>
“改代碼,加日志,重新發(fā)布一次?!?/p>
“懷疑是線程池的問題,重啟會破壞現(xiàn)場?!?/p>
長達幾十秒的沉默之后:“據(jù)說,排查問題的最高境界,就是只通過Review代碼來發(fā)現(xiàn)問題?!?/p>
比幾十秒長幾十倍的沉默之后:“我輪詢了那段代碼一十七遍之后,終于得出一個結(jié)論?!?/p>
“結(jié)論是?”
Java對象行為
文章開頭的問題本質(zhì)上是動態(tài)改變內(nèi)存中已存在對象的行為問題。
所以,得先弄清楚JVM中和對象行為有關(guān)的地方在哪里,有沒有更改的可能性。
對象使用兩種東西來描述事物:行為和屬性。
舉個例子:
public?class?Person{
??private?int?age;
??private?String?name;
??public?void?speak(String?str)?{
????System.out.println(str);
?}
? public?Person(int?age,?String?name)?{
????this.age?=?age;
????this.name?=?name;
? }
}
Person?personA?=?new?Person(43,?"lixunhuan");
personA.speak("我是李尋歡");
Person?personB?=?new?Person(23,?"afei");
personB.speak("我是阿飛");
Method area is created on virtual machine startup, shared among all Java virtual machine threads and it is logically part of heap area. It stores per-class structures such as the run-time constant pool, field and method data, and the code for methods and constructors.
java.lang.instrument.Instrumentation?!?/section>java.lang.instrument.Instrumentation
看完文檔之后,我們發(fā)現(xiàn)這么兩個接口:redefineClasses和retransformClasses。一個是重新定義class,一個是修改class。這兩個大同小異,看redefineClasses的說明:
This method is used to replace the definition of a class without reference to the existing class file bytes, as one might do when recompiling from source for fix-and-continue debugging. Where the existing class file bytes are to be transformed (for example in bytecode instrumentation) retransformClasses should be used.
The redefinition may change method bodies, the constant pool and attributes. The redefinition must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions. The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception.
我們能做的基本上也就是簡單修改方法內(nèi)的一些行為,這對于我們開頭的問題,打印一段日志來說,已經(jīng)足夠了。當(dāng)然,我們除了通過retransform來打印日志,還能做很多其他非常有用的事情,這個下文會進行介紹。
那怎么得到我們需要的class文件呢?一個最簡單的方法,是把修改后的Java文件重新編譯一遍得到class文件,然后調(diào)用redefineClasses替換。但是對于沒有(或者拿不到,或者不方便修改)源碼的文件我們應(yīng)該怎么辦呢?其實對于JVM來說,不管是Java也好,Scala也好,任何一種符合JVM規(guī)范的語言的源代碼,都可以編譯成class文件。JVM的操作對象是class文件,而不是源碼。所以,從這種意義上來講,我們可以說“JVM跟語言無關(guān)”。既然如此,不管有沒有源碼,其實我們只需要修改class文件就行了。
直接操作字節(jié)碼
BTrace
在我們的工程中,誰來做這個尋找字節(jié)碼,修改字節(jié)碼,然后retransform的動作呢?我們并非先知,不可能知道未來有沒有可能遇到文章開頭的這種問題??紤]到性價比,我們也不可能在每個工程中都開發(fā)一段專門做這些修改字節(jié)碼、重新加載字節(jié)碼的代碼。
如果JVM不在本地,在遠程呢?
如果連ASM都不會用呢?能不能更通用一些,更“傻瓜”一些。
A safe, dynamic tracing tool for the Java platform.
package?com.sun.btrace.samples;
import?com.sun.btrace.annotations.*;
import?com.sun.btrace.AnyType;
import?static?com.sun.btrace.BTraceUtils.*;
/**
?*?This?sample?demonstrates?regular?expression
?*?probe?matching?and?getting?input?arguments
?*?as?an?array?-?so?that?any?overload?variant
?*?can?be?traced?in?"one?place".?This?example
?*?traces?any?"readXX"?method?on?any?class?in
?*?java.io?package.?Probed?class,?method?and?arg
?*?array?is?printed?in?the?action.
?*/
@BTrace?public?class?ArgArray?{
????@OnMethod(
????????clazz="/java\\.io\\..*/",
????????method="/read.*/"
????)
????public?static?void?anyRead(@ProbeClassName?String?pcn,?@ProbeMethodName?String?pmn,?AnyType[]?args)?{
????????println(pcn);
????????println(pmn);
????????printArray(args);
????}
}
再來看另一個例子:每隔2秒打印截止到當(dāng)前創(chuàng)建過的線程數(shù)。
package?com.sun.btrace.samples;
import?com.sun.btrace.annotations.*;
import?static?com.sun.btrace.BTraceUtils.*;
import?com.sun.btrace.annotations.Export;
/**
?*?This?sample?creates?a?jvmstat?counter?and
?*?increments?it?everytime?Thread.start()?is
?*?called.?This?thread?count?may?be?accessed
?*?from?outside?the?process.?The?@Export?annotated
?*?fields?are?mapped?to?jvmstat?counters.?The?counter
?*?name?is?"btrace."?+??+?"."?+?
?*/ ?
@BTrace?public?class?ThreadCounter?{
????//?create?a?jvmstat?counter?using?@Export
????@Export?private?static?long?count;
????@OnMethod(
????????clazz="java.lang.Thread",
????????method="start"
????)?
????public?static?void?onnewThread(@Self?Thread?t)?{
????????//?updating?counter?is?easy.?Just?assign?to
????????//?the?static?field!
????????count++;
????}
????@OnTimer(2000)?
????public?static?void?ontimer()?{
????????//?we?can?access?counter?as?"count"?as?well
????????//?as?from?jvmstat?counter?directly.
????????println(count);
????????//?or?equivalently?...
????????println(Counters.perfLong("btrace.com.sun.btrace.samples.ThreadCounter.count"));
????}
}
BTrace主要有下面幾個模塊:
BTrace腳本:利用BTrace定義的注解,我們可以很方便地根據(jù)需要進行腳本的開發(fā)。
Compiler:將BTrace腳本編譯成BTrace class文件。
Client:將class文件發(fā)送到Agent。
Agent:基于Java的Attach API,Agent可以動態(tài)附著到一個運行的JVM上,然后開啟一個BTrace Server,接收client發(fā)過來的BTrace腳本;解析腳本,然后根據(jù)腳本中的規(guī)則找到要修改的類;修改字節(jié)碼后,調(diào)用Java Instrument的retransform接口,完成對對象行為的修改并使之生效。另外,搜索公眾號互聯(lián)網(wǎng)架構(gòu)師后臺回復(fù)“2T”,獲取一份驚喜禮包。
整個BTrace的架構(gòu)大致如下:

不允許創(chuàng)建對象
不允許創(chuàng)建數(shù)組
不允許拋異常
不允許catch異常
不允許隨意調(diào)用其他對象或者類的方法,只允許調(diào)用com.sun.btrace.BTraceUtils中提供的靜態(tài)方法(一些數(shù)據(jù)處理和信息輸出工具)
不允許改變類的屬性
不允許有成員變量和方法,只允許存在static public void方法
不允許有內(nèi)部類、嵌套類
不允許有同步方法和同步塊
不允許有循環(huán)
不允許隨意繼承其他類(當(dāng)然,java.lang.Object除外)
不允許實現(xiàn)接口
不允許使用assert
不允許使用Class對象
Arthas
本文旨在說明Java動態(tài)追蹤技術(shù)的來龍去脈,掌握技術(shù)背后的原理之后,只要愿意,各位讀者也可以開發(fā)出自己的“冰封王座”出來。
三生萬物
相關(guān)閱讀:2T架構(gòu)師學(xué)習(xí)資料干貨分享
全棧架構(gòu)社區(qū)交流群
?「全棧架構(gòu)社區(qū)」建立了讀者架構(gòu)師交流群,大家可以添加小編微信進行加群。歡迎有想法、樂于分享的朋友們一起交流學(xué)習(xí)。
看完本文有收獲?請轉(zhuǎn)發(fā)分享給更多人
往期資源:
