厲害了!不重啟JVM,替換掉已經(jīng)加載的類
在遙遠(yuǎn)的希艾斯星球爪哇國(guó)塞沃城中,兩名年輕的程序員正在為一件事情苦惱,程序出問題了,一時(shí)看不出問題出在哪里,于是有了以下對(duì)話:
“Debug一下吧?!?/p>
“線上機(jī)器,沒開Debug端口?!?/p>
“看日志,看看請(qǐng)求值和返回值分別是什么?”
“那段代碼沒打印日志?!?/p>
“改代碼,加日志,重新發(fā)布一次。”
“懷疑是線程池的問題,重啟會(huì)破壞現(xiàn)場(chǎng)?!?/p>
長(zhǎng)達(dá)幾十秒的沉默之后:“據(jù)說,排查問題的最高境界,就是只通過Review代碼來發(fā)現(xiàn)問題?!?/p>
比幾十秒長(zhǎng)幾十倍的沉默之后:“我輪詢了那段代碼一十七遍之后,終于得出一個(gè)結(jié)論。”
“結(jié)論是?”
Java對(duì)象行為
文章開頭的問題本質(zhì)上是動(dòng)態(tài)改變內(nèi)存中已存在對(duì)象的行為問題。
所以,得先弄清楚JVM中和對(duì)象行為有關(guān)的地方在哪里,有沒有更改的可能性。
對(duì)象使用兩種東西來描述事物:行為和屬性。
舉個(gè)例子:
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)這么兩個(gè)接口:redefineClasses和retransformClasses。一個(gè)是重新定義class,一個(gè)是修改class。這兩個(gè)大同小異,看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.
我們能做的基本上也就是簡(jiǎn)單修改方法內(nèi)的一些行為,這對(duì)于我們開頭的問題,打印一段日志來說,已經(jīng)足夠了。當(dāng)然,我們除了通過retransform來打印日志,還能做很多其他非常有用的事情,這個(gè)下文會(huì)進(jìn)行介紹。
那怎么得到我們需要的class文件呢?一個(gè)最簡(jiǎn)單的方法,是把修改后的Java文件重新編譯一遍得到class文件,然后調(diào)用redefineClasses替換。但是對(duì)于沒有(或者拿不到,或者不方便修改)源碼的文件我們應(yīng)該怎么辦呢?其實(shí)對(duì)于JVM來說,不管是Java也好,Scala也好,任何一種符合JVM規(guī)范的語言的源代碼,都可以編譯成class文件。JVM的操作對(duì)象是class文件,而不是源碼。所以,從這種意義上來講,我們可以說“JVM跟語言無關(guān)”。既然如此,不管有沒有源碼,其實(shí)我們只需要修改class文件就行了。
直接操作字節(jié)碼
BTrace
在我們的工程中,誰來做這個(gè)尋找字節(jié)碼,修改字節(jié)碼,然后retransform的動(dòng)作呢?我們并非先知,不可能知道未來有沒有可能遇到文章開頭的這種問題??紤]到性價(jià)比,我們也不可能在每個(gè)工程中都開發(fā)一段專門做這些修改字節(jié)碼、重新加載字節(jié)碼的代碼。
如果JVM不在本地,在遠(yuǎn)程呢?
如果連ASM都不會(huì)用呢?能不能更通用一些,更“傻瓜”一些。
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);
}
}
再來看另一個(gè)例子:每隔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." + <className> + "." + <fieldName>
*/
@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主要有下面幾個(gè)模塊:
BTrace腳本:利用BTrace定義的注解,我們可以很方便地根據(jù)需要進(jìn)行腳本的開發(fā)。
Compiler:將BTrace腳本編譯成BTrace class文件。
Client:將class文件發(fā)送到Agent。
Agent:基于Java的Attach API,Agent可以動(dòng)態(tài)附著到一個(gè)運(yùn)行的JVM上,然后開啟一個(gè)BTrace Server,接收client發(fā)過來的BTrace腳本;解析腳本,然后根據(jù)腳本中的規(guī)則找到要修改的類;修改字節(jié)碼后,調(diào)用Java Instrument的retransform接口,完成對(duì)對(duì)象行為的修改并使之生效。另外,搜索公眾號(hào)互聯(lián)網(wǎng)架構(gòu)師后臺(tái)回復(fù)“2T”,獲取一份驚喜禮包。
整個(gè)BTrace的架構(gòu)大致如下:

不允許創(chuàng)建對(duì)象
不允許創(chuàng)建數(shù)組
不允許拋異常
不允許catch異常
不允許隨意調(diào)用其他對(duì)象或者類的方法,只允許調(diào)用com.sun.btrace.BTraceUtils中提供的靜態(tài)方法(一些數(shù)據(jù)處理和信息輸出工具)
不允許改變類的屬性
不允許有成員變量和方法,只允許存在static public void方法
不允許有內(nèi)部類、嵌套類
不允許有同步方法和同步塊
不允許有循環(huán)
不允許隨意繼承其他類(當(dāng)然,java.lang.Object除外)
不允許實(shí)現(xiàn)接口
不允許使用assert
不允許使用Class對(duì)象
Arthas
本文旨在說明Java動(dòng)態(tài)追蹤技術(shù)的來龍去脈,掌握技術(shù)背后的原理之后,只要愿意,各位讀者也可以開發(fā)出自己的“冰封王座”出來。
三生萬物
3、心態(tài)崩了!稅前2萬4,到手1萬4,年終獎(jiǎng)扣稅方式1月1日起施行~
4、雷軍做程序員時(shí)寫的博客,很強(qiáng)大!
5、人臉識(shí)別的時(shí)候,一定要穿上衣服??!
