一、簡介:
在生產環境中經常遇到格式各樣的問題,如OOM或者莫名其妙的進程死掉。一般情況下是通過修改程序,添加打印日志;然后重新發布程序來完成。然而,這不僅麻煩,而且帶來很多不可控的因素。有沒有一種方式,在不修改原有運行程序的情況下獲取運行時的數據信息呢?如方法參數、返回值、全局變量、堆棧信息等。Btrace就是這樣一個工具,它可以在不修改原有代碼的情況下動態地追蹤java運行程序,通過hotswap技術,動態將跟蹤字節碼注入到運行類中,對運行代碼侵入較小,對性能上的影響可以忽略不計。
在下列情況時可以使用BTrace進行分析:
1、接口性能變慢,分析每個方法的耗時情況;
2、當在Map中插入大量數據,分析其擴容情況;
3、分析哪個方法調用了System.gc(),調用棧如何;
4、執行某個方法拋出異常時,分析運行時參數;
5、..................
二、安裝:
1、安裝JDK;
2、下載BTrace的壓縮包,這里使用的是BTrace 1.3.11版本,可以到下面地址下載:
http://www.voidcn.com/link?url=https://github.com/btraceio/btrace/releases/tag/v1.3.11
3、將BTrace包解壓,在系統的環境變量上添加變量BTRACE_HOME,並設置其路徑為BTrace的路徑,同時在PATH變量中添加上路徑%BTRACE_HOME%\bin;
4、編輯%BTRACE_HOME%\bin\btrace.bat文件,將其中的-Dcom.sun.btrace.unsafe=false改為-Dcom.sun.btrace.unsafe=true;
5、btrace命令的語法說明:
btrace [-I <include-path>] [-p <port>] [-cp <classpath>] <pid> <btrace-script> [<args>]
1)沒有這個表明跳過預編譯;
2)include-path:指定用來編譯腳本的頭文件路徑(關於預編譯可參考例子ThreadBean.java);
3)port:btrace agent端口,默認是2020;
4)classpath:編譯所需類路徑,一般是指btrace-client.jar等類所在路徑;
5)pid:java進程id;
6)btrace-script:btrace腳本可以是.java文件,也可以是.class文件;
7)args:傳遞給btrace腳本的參數, 在腳本中可以通過$(), $length()來獲取這些參數(定義在BTraceUtils中);
三、Demo:
(一)JavaSE應用Demo:
1、編寫測試功能實現類:
public class Calculator {
public int add(int a, int b) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } return a + b; } } |
2、編寫調用代碼:
public class App { public static void main( String[] args ) { Calculator calc = new Calculator(); Random random = new Random(); while (true) { int a = random.nextInt(10); int b = random.nextInt(20); int c = calc.add(a, b); System.out.println(String.format("%d + %d = %d", a, b, c)); } } } |
上面的代碼無限循環調用Calculator .add方法並輸出調用結果;
3、運行程序,可以看到屏幕上不停的輸出各種加法運算的表達式;
4、編寫btrace腳本:
@BTrace(unsafe = true) public class BTraceTest { @OnMethod(clazz = "com.ucar.test.Calculator", method = "add", location = @Location(Kind.RETURN)) public static void traceTest(int a, int b, @Return int sum) { println(String.format("%d + %d = %d", a, b, sum)); } } |
@BTrace注解中要加上unsafe=true,否則運行btrace腳本時會因為安全機制導致報錯而無法執行腳本;
@OnMethod注解中的clazz表示要跟蹤的類名,method表示要跟蹤的方法名稱,location表示在什么時候進行攔截;
5、運行btrace腳本,可以看到前面輸出的加法運算表達式也能在這個窗口上輸出;
運行btrace腳本的命令為:
btrace 3856 BTraceTest.java
其中3856為剛才運行的java程序的進程ID;
(二)web應用Demo:
1、新建SpringMVC的web應用程序(參考https://www.cnblogs.com/laoxia/p/9311442.html);
2、實現Controller:
@RestController @RequestMapping("/btrace") public class BTraceController {
@RequestMapping("/arg1") public String arg1(@RequestParam("name") String name) { return "hello: " + name; } } |
3、生成war包並放到tomcat的webapp目錄下,啟動tomcat,瀏覽器中打開URL地址:http://localhost:8080/test/btrace/arg1?name=aaaaa,頁面上應該能正常打印出“hello: aaaaa”;
4、編寫BTrace腳本:
@BTrace(unsafe = true) public class PrintArgSimple {
@OnMethod(clazz = "com.ucar.test.controller.BTraceController", method = "arg1", location = @Location(Kind.RETURN)) public static void anyRead(@ProbeClassName String pcn, // 被攔截的類名 @ProbeMethodName String pmn, //被攔截的方法名 AnyType[] args //被攔截的方法的參數值) { BTraceUtils.printArray(args); BTraceUtils.println("className: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println(); } } |
注意:需要在maven的POM文件中引入btrace-client.jar, btrace-boot.jar和btrace_agent.jar三個文件或者直接引入這三個jar包;
5、運行腳本,然后在瀏覽器中請求第三步的URL地址,這時候就能看到屏幕上打印出運行過程中的相關信息;
運行BTrace的命令為:
btrace 1256 PrintArgSimple.java
其中1256為這個web應用進程的進程ID;
6、注意:需要將web應用打包成war放到tomcat下運行,如果直接在idea下運行會報錯;
四、攔截時機:
1、Kind.ENTRY:入口攔截,默認值;
2、Kind.RETURN:攔截返回值,只有把攔截位置定義為Kind.RETURN,才能獲取方法的返回結果@Return和執行時間@Duration;
3、Kind.THROW:發生異常時攔截;
4、Kind.LINE:攔截某一行,可以監控代碼是否執行到指定的位置;
5、Kind.CALL:分析方法中調用其它方法的執行情況,比如在execute方法中,想獲取add方法的執行耗時,必須把where設置成Where.AFTER;
五、技巧:
1、攔截構造函數:
指定method = "<init>"即可攔截指定類的構造函數;
2、攔截同名函數:攔截同名重載方法,只需要在BTrace腳本的方法中聲明與之對應的參數即可。
比如有如下兩個同名方法:
@RequestMapping("/same1") public String same(@RequestParam("name") String name) { return "hello: " + name; }
@RequestMapping("/same2") public User same(@RequestParam("id") int id, @RequestParam("name") String name) { return new User(id, name); } |
編寫如下的btrace腳本即可攔截:
@OnMethod(clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "same") public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, String name) { BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println("name: " + name); BTraceUtils.println(); }
@OnMethod(clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "same") public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int id, String name) { BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println("id: " + id); BTraceUtils.println("name: " + name); BTraceUtils.println(); } |
3、攔截返回值:
指定location=@Location(Kind.RETURN),並且在方法的參數里面加上@Return AnyType result即可接收返回值;
4、攔截異常:
@BTrace public class PrintOnThrow { @TLS static Throwable currentException;
@OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow(@Self Throwable self) { // @Self其實就是攔截了this currentException = self; }
@OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow1(@Self Throwable self, String s) { currentException = self; }
@OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow1(@Self Throwable self, String s, Throwable cause) { currentException = self; }
@OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow2(@Self Throwable self, Throwable cause) { currentException = self; }
@OnMethod( clazz="java.lang.Throwable", method="<init>", location=@Location(Kind.RETURN) ) public static void onthrowreturn() { if (currentException != null) { // 打印異常堆棧 BTraceUtils.Threads.jstack(currentException); BTraceUtils.println("====================="); // 打印完之后就置空 currentException = null; } } } |
在命令行里運行該腳本,訪問相應的接口后,即可輸出異常堆棧;即使異常被try catch給隱藏起來了,這個腳本也一樣能揪出來。
5、攔截指定行:
@BTrace public class PrintLine { @OnMethod( clazz="org.zero01.monitor_tuning.controller.BTraceController", method="exception", location=@Location(value=Kind.LINE, line=43) // 攔截第43行 ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, int line) { BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println("line: " + line); BTraceUtils.println(); } } |
如果沒有任何輸出的話,就代表那一行沒有被執行到,所以沒被攔截。這種攔截某一行的方式,不適用於判斷是否有異常,只能單純用於判斷某一行是否被執行了。
6、攔截復雜參數:
比如要攔截下面方法的復雜參數類型User:
@RequestMapping("/arg2") public User arg2(User user) { return user; } |
可以使用下面的btrace腳本攔截:
@BTrace public class PrintArgComplex { @OnMethod( clazz = "org.zero01.monitor_tuning.controller.BTraceController", method = "arg2", location = @Location(Kind.ENTRY) ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn, User user) { //print all fields BTraceUtils.print("print all fields: "); BTraceUtils.printFields(user);
//print one field Field oneFiled = BTraceUtils.field("org.zero01.monitor_tuning.vo.User", "name"); BTraceUtils.println("print one field: " + BTraceUtils.get(oneFiled, user));
BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println(); } } |
7、攔截環境變量:
@BTrace public class PrintJinfo { static { // 打印系統屬性 BTraceUtils.println("System Properties:"); BTraceUtils.printProperties();
// 打印JVM參數 BTraceUtils.println("VM Flags:"); BTraceUtils.printVmArguments();
// 打印環境變量 BTraceUtils.println("OS Enviroment:"); BTraceUtils.printEnv();
// 退出腳本 BTraceUtils.exit(0); } } |
8、使用正則表達式攔截:
@BTrace public class PrintRegex { @OnMethod( // 類名也可以使用正則表達式進行匹配 clazz = "org.zero01.monitor_tuning.controller.BTraceController", // 正則表達式需要寫在兩個斜杠內 method = "/.*/" ) public static void anyRead(@ProbeClassName String pcn, @ProbeMethodName String pmn) { BTraceUtils.println("ClassName: " + pcn); BTraceUtils.println("MethodName: " + pmn); BTraceUtils.println(); } } |
六、注意事項:
1、@ProbeClassName String clazz:此處String不能寫為java.lang.String;
2、@OnMethod(clazz="com.alibaba.security.acl.support.PermissionFactory", method="createPermission", type="com.alibaba.security.acl.support.AbstractPermission(java.lang.String,java.lang.String,com.alibaba.security.acl.support.PermissionDefiner)")
此處得String必須寫成java.lang.String;
3、BTrace腳本默認只能本地運行,也就是只能調試本地的Java進程。如果需要在本地調試遠程的Java進程的話,是需要自己去修改BTrace源碼的;
4、BTrace腳本在生產環境下可以使用,但是被修改的字節碼不會被還原。所以我們需要先在本地調試好BTrace腳本,然后才能放到生產環境下使用。並且需要注意BTrace腳本中不能含有影響性能或消耗資源較多的代碼,不然會導致線上的服務性能降低。
七、其他:
1、其他命令行工具說明:
(1) Btracec:用於預編譯BTrace腳本,用於在編譯時期驗證腳本正確性。
btracec [-I <include-path>] [-cp <classpath>] [-d <directory>] <one-or-more-BTrace-.java-files>
參數意義同btrace命令一致,directory表示編譯結果輸出目錄。
(2) Btracer:btracer命令同時啟動應用程序和BTrace腳本,即在應用程序啟動過程中使用BTrace腳本。而btrace命令針對已運行程序執行BTrace腳本。
btracer <pre-compiled-btrace.class> <application-main-class> <application-args>
參數說明:
pre-compiled-btrace.class表示經過btracec編譯后的BTrace腳本。
application-main-class表示應用程序代碼;
application-args表示應用程序參數。
2、方法上的注解:
(1) @ OnMethod用來指定trace的目標類和方法以及具體位置,被注解的方法在匹配的方法執行到指定的位置會被調用。
- "clazz"屬性用來指定目標類名,可以指定全限定類名,比如"java.awt.Component",也可以是正則表達式(表達式必須寫在"//"中,比如"/java\\.awt\\..+/")。
- "method"屬性用來指定被trace的方法.表達式可以參考自帶的例子(NewComponent.java和Classload.java,關於方法的注解可以參考MultiClass.java)。
- 有時候被trace的類和方法可能也使用了注解.用法參考自帶例子WebServiceTracker.java。
- 針對注解也是可以使用正則表達式,比如像這個"@/com\\.acme\\..+/ ",也可以通過指定超類來匹配多個類,比如"+java.lang.Runnable"可以匹配所有實現了java.lang.Runnable接口的類.具體參考自帶例子SubtypeTracer.java。
(2) @OnTimer定時觸發Trace,時間可以指定,單位為毫秒,具體參考自帶例子Histogram.java。
(3) @OnError當trace代碼拋異常或者錯誤時,該注解的方法會被執行.如果同一個trace腳本中其他方法拋異常,該注解方法也會被執行。
(4) @OnExit當trace方法調用內置exit(int)方法(用來結束整個trace程序)時,該注解的方法會被執行.參考自帶例子ProbeExit.java。
(5) @OnEvent用來截獲"外部"btrace client觸發的事件,比如按Ctrl-C中斷btrace執行時,並且選擇2,或者輸入事件名稱,將執行使用了該注解的方法,該注解的value值為具體事件名稱。具體參考例子HistoOnEvent.java;
(6) @OnLowMemory當內存超過某個設定值將觸發該注解的方法,具體參考MemAlerter.java;
(7) @OnProbe使用外部文件XML來定義trace方法以及具體的位置,具體參考示例SocketTracker1.java和java.net.socket.xml。
3、參數上的注解:
- @Self用來指定被trace方法的this,可參考例子AWTEventTracer.java和AllCalls1.java
- @Return用來指定被trace方法的返回值,可參考例子Classload.java
- @ProbeClassName (since 1.1)用來指定被trace的類名,可參考例子AllMethods.java
- @ProbeMethodName (since 1.1)用來指定被trace的方法名,可參考例子WebServiceTracker.java。
- @TargetInstance (since 1.1)用來指定被trace方法內部被調用到的實例,可參考例子AllCalls2.java
- @TargetMethodOrField (since 1.1)用來指定被trace方法內部被調用的方法名,可參考例子AllCalls1.java和AllCalls2.java。
4、屬性上的注解:
- @Export該注解的靜態屬性主要用來與jvmstat計數器做關聯, 使用該注解之后,btrace程序就可以向jvmstat客戶端(可以用來統計jvm堆中的內存使用量)暴露trace程序的執行次數, 具體可參考例子ThreadCounter.java。
- @Property使用了該注解的trace腳本將作為MBean的一個屬性,一旦使用該注解, trace腳本就會創建一個MBean並向MBean服務器注冊, 這樣JMX客戶端比如VisualVM, jconsole就可以看到這些BTrace MBean, 如果這些被注解的屬性與被trace程序的屬性關聯, 那么就可以通過VisualVM和jconsole來查看這些屬性了, 具體可參考例子ThreadCounterBean.java和HistogramBean.java。
- @TLS用來將一個腳本變量與一個ThreadLocal變量關聯, 因為ThreadLocal變量是跟線程相關的, 一般用來檢查在同一個線程調用中是否執行到了被trace的方法, 具體可參考例子OnThrow.java和WebServiceTracker.java。
5、類上的注解:
- @com.sun.btrace.annotations.DTrace用來指定btrace腳本與內置在其腳本中的D語言腳本關聯, 具體參考例子DTraceInline.java。
- @com.sun.btrace.annotations.DTraceRef用來指定btrace腳本與另一個D語言腳本文件關聯, 具體參考例子DTraceRefDemo.java。
- @com.sun.btrace.annotations.BTrace用來指定該java類為一個btrace腳本文件。
6、BTrace文件下的samples文件夾下包含了很多的示例,這些示例說明如下:
AWTEventTracer.java -演示了對EventQueue.dispatchEvent()事件進行trace的做法,可以通過instanceof來對事件進行過濾,比如這里只針對focus事件trace. AllLines.java -演示了如何在被trace的程序到達probe指定的類和指定的行號時執行指定的操作(例子中指定的行號是-1表示任意行). AllSync.java -演示了如何在進入/退出同步塊進行trace. ArgArray.java -演示了打印java.io包下所有類的readXXX方法的輸入參數. Classload.java -演示打印成功加載指定類以及堆棧信息. CommandArg.java -演示如何獲取btrace命令行參數. Deadlock.java -演示了@OnTimer注解和內置deadlock()方法的用法 DTraceInline.java -演示@DTrace注解的用法 DTraceDemoRef.java -演示@DTraceRef注解的用法. FileTracker.java -演示了如何對File{Input/Output}Stream構造函數中初始化打開文件的讀寫文件操作進行trace. FinalizeTracker.java -演示了如何打印一個類所有的屬性,這個在調試和故障分析中非常有用.這里的例子是打印FileInputStream類的close() /finalize()方法被調用時的信息. Histogram.java -演示了統計javax.swing.JComponent在一個應用中被創建了多少次. HistogramBean.java -同上例,只不過演示了如何與JMX集成,這里的map屬性通過使用@Property注解被暴露成一個MBean. HistoOnEvent.java -同上例,只不過演示了如何在通過按ctrl+c中斷當前腳本時打印出創建次數,而不是定時打印. JdbcQueries.java -演示了聚合(aggregation)功能.關於聚合功能可參考DTrace. JInfo.java -演示了內置方法printVmArguments(), printProperties()和printEnv()的用法 JMap.java -演示了內置方法dumpHeap()的用法.即將目標應用的堆信息以二進制的形式dump出來 JStack.java -演示了內置方法jstackAll()的用法,即打印所有線程的堆棧信息. LogTracer.java -演示了如何深入實例方法(Logger.log)並調用內置方法(field() )打印私有屬性內容. MemAlerter.java -演示了使用@OnLowMememory注解監控內存使用情況.即堆內存中的年老代達到指定值時打印出內存信息. Memory.java -演示每隔4s打印一次內存統計信息. MultiClass.java -演示了通過使用正則表達式對多個類的多個方法進行trace. NewComponent.java -使用計數器每隔一段時間檢查當前應用中創建java.awt.Component的個數. OnThrow.java -當拋出異常時,打印出異常堆棧信息. ProbeExit.java -演示@OnExit注解和內置exit(int)方法的用法 Profiling.java -演示了對profile的支持. //我執行沒成功, BTrace內部有異常 Sizeof.java -演示了內置的sizeof方法的使用. SocketTracker.java -演示了對socket的creation/bind方法的trace. SocketTracker1.java -同上,只不過使用了@OnProbe. SysProp.java -演示了使用內置方法獲取系統屬性,這里是對java.lang.System的getProperty方法進行trace. SubtypeTracer.java -演示了如何對指定超類的所有子類的指定方法進行trace. ThreadCounter.java -演示了在腳本中如何使用jvmstat計數器. (jstat -J-Djstat.showUnsupported=true -name btrace.com.sun.btrace.samples.ThreadCounter.count需要這樣來從外部通過jstat來訪問) ThreadCounterBean.java -同上,只不過使用了JMX. ThreadBean.java -演示了對預編譯器的使用(並結合了JMX). ThreadStart.java -演示了腳本中DTrace的用法. Timers.java -演示了在一個腳本中同時使用多個@OnTimer URLTracker.java -演示了在每次URL.openConnection成功返回時打印出url.這里也使用了D語言腳本. WebServiceTracker.java -演示了如何根據注解進行trace. |
7、參考文檔:
http://huanghaifeng1990.iteye.com/blog/2121419
http://agapple.iteye.com/blog/962119
http://agapple.iteye.com/blog/1005918