Java 動態字節碼技術

對 Debug 的好奇


初學 Java 時,我對 IDEA 的 Debug 很是好奇,不止是它能查看斷點的上下文環境,更神奇的是我能夠在斷點處使用它的 Evaluate 功能直接執行某些命令,進行一些計算或改變當前變量。html

剛開始語法不熟常常寫錯代碼,從新打包部署一次代碼耗時很長,我就直接面向 Debug 開發。在要編寫的方法開始處打一個斷點,在 Evaluate 框內一次次地執行方法函數不停地調整代碼,沒問題後再將代碼複製出來放到 IDEA 裏,再進行下一個方法的編寫,這樣就跟寫 PHP 相似的解釋性語言同樣,寫完即執行,很是方便。java

但 Java 是靜態語言,運行以前是要先進行編譯的,難道我寫的這些代碼是被實時編譯又」注入」到我正在 Debug 的服務裏了嗎?linux

隨着對 Java 的越發熟悉,我也瞭解了反射、字節碼等技術,直到前些天的週會分享,有位同事分享了 Btrace 的使用和實現,提到了 Java 的 ASM 框架和 JVM TI 接口。 Btrace 修改代碼能力的實現與 Debug 的 Evaluate 有不少類似之處,這大大吸引了我。分享就像一個引子,從中學到的東西只是皮毛,要了解它仍是要本身研究。因而本身查看資料並寫代碼學習了下其具體實現。git

轉載隨意,文章會持續修訂,請註明來源地址:https://zhenbianshu.github.io 。github

ASM


實現 Evaluate 要解決的第一個問題就是怎麼改變原有代碼的行爲,它的實如今 Java 裏被稱爲動態字節碼技術。apache

動態生成字節碼

咱們知道,咱們編寫的 Java 代碼都是要被編譯成字節碼後才能放到 JVM 裏執行的,而字節碼一旦被加載到虛擬機中,就能夠被解釋執行。api

字節碼文件(.class)就是普通的二進制文件,它是經過 Java 編譯器生成的。而只要是文件就能夠被改變,若是咱們用特定的規則解析了原有的字節碼文件,對它進行修改或者乾脆從新定義,這不就能夠改變代碼行爲了麼。數組

Java 生態裏有不少能夠動態生成字節碼的技術,像 BCEL、Javassist、ASM、CGLib 等,它們各有本身的優點。有的使用複雜卻功能強大、有的簡單確也性能些差。oracle

ASM 框架

ASM 是它們中最強大的一個,使用它能夠動態修改類、方法,甚至能夠從新定義類,連 CGLib 底層都是用 ASM 實現的。框架

固然,它的使用門檻也很高,使用它須要對 Java 的字節碼文件有所瞭解,熟悉 JVM 的編譯指令。雖然我對 JVM 的字節碼語法不熟,但有大神開發了能夠在 IDEA 裏查看字節碼的插件:ASM Bytecode Outline ,在要查看的類文件裏右鍵選擇 Show bytecode Outline 便可以右側的工具欄查看咱們要生成的字節碼。對照着示例,咱們就能夠很輕鬆地寫出操做字節碼的 Java 代碼了。

而切到 ASMified 標籤欄,咱們甚至能夠直接獲取到 ASM 的使用代碼。

經常使用方法

在 ASM 的代碼實現裏,最明顯的就是訪問者模式,ASM 將對代碼的讀取和操做都包裝成一個訪問者,在解析 JVM 加載到的字節碼時調用。

ClassReader 是 ASM 代碼的入口,經過它解析二進制字節碼,實例化時它時,咱們須要傳入一個 ClassVisitor,在這個 Visitor 裏,咱們能夠實現 visitMethod()/visitAnnotation() 等方法,用以定義對類結構(如方法、字段、註解)的訪問方法。

而 ClassWriter 接口繼承了 ClassVisitor 接口,咱們在實例化類訪問器時,將 ClassWriter 「注入」 到裏面,以實現對類寫入的聲明。

Instrument


介紹

字節碼是修改完了,但是 JVM 在執行時會使用本身的類加載器加載字節碼文件,加載後並不會理會咱們作出的修改,要想實現對現有類的修改,咱們還須要搭配 Java 的另外一個庫 instrument

instrument 是 JVM 提供的一個能夠修改已加載類文件的類庫。1.6之前,instrument 只能在 JVM 剛啓動開始加載類時生效,以後,instrument 更是支持了在運行時對類定義的修改。

使用

要使用 instrument 的類修改功能,咱們須要實現它的 ClassFileTransformer 接口定義一個類文件轉換器。它惟一的一個 transform() 方法會在類文件被加載時調用,在 transform 方法裏,咱們能夠對傳入的二進制字節碼進行改寫或替換,生成新的字節碼數組後返回,JVM 會使用 transform 方法返回的字節碼數據進行類的加載。

JVM TI


定義完了字節碼的修改和重定義方法,但咱們怎麼才能讓 JVM 可以調用咱們提供的類轉換器呢?這裏又要介紹到 JVM TI 了。

介紹

JVM TI(JVM Tool Interface)JVM 工具接口是 JVM 提供的一個很是強大的對 JVM 操做的工具接口,經過這個接口,咱們能夠實現對 JVM 多種組件的操做,從JVMTM Tool Interface 這裏咱們認識到 JVM TI 的強大,它包括了對虛擬機堆內存、類、線程等各個方面的管理接口。

JVM TI 經過事件機制,經過接口註冊各類事件勾子,在 JVM 事件觸發時同時觸發預約義的勾子,以實現對各個 JVM 事件的感知和反應。

Agent

Agent 是 JVM TI 實現的一種方式。咱們在編譯 C 項目裏連接靜態庫,將靜態庫的功能注入到項目裏,從而才能夠在項目裏引用庫裏的函數。咱們能夠將 agent 類比爲 C 裏的靜態庫,咱們也能夠用 C 或 C++ 來實現,將其編譯爲 dll 或 so 文件,在啓動 JVM 時啓動。

這時再來思考 Debug 的實現,咱們在啓動被 Debug 的 JVM 時,必須添加參數 -agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:3333,而 -agentlib 選項就指定了咱們要加載的 Java Agent,jdwp 是 agent 的名字,在 linux 系統中,咱們能夠在 jre 目錄下找到 jdwp.so 庫文件。

Java 的調試體系 jdpa 組成,從高到低分別爲 jdi->jdwp->jvmti,咱們經過 JDI 接口發送調試指令,而 jdwp 就至關於一個通道,幫咱們翻譯 JDI 指令到 JVM TI,最底層的 JVM TI 最終實現對 JVM 的操做。

使用

JVM TI 的 agent 使用很簡單,在啓動 agent 時添加 -agent 參數指定咱們要加載的 agent jar包便可。

而要實現代碼的修改,咱們須要實現一個 instrument agent,它能夠經過在一個類裏添加 premain() 或 agentmain() 方法來實現。而要實現 1.6 以上的動態 instrument 功能,實現 agentmain 方法便可。

在 agentmain 方法裏,咱們調用 Instrumentation.retransformClasses() 方法實現對目標類的重定義。

另外往一個正在運行的 JVM 裏動態添加 agent,還須要用到 JVM 的 attach 功能,Sun 公司的 tools.jar 包裏包含的 VirtualMachine 類提供了 attach 一個本地 JVM 的功能,它須要咱們傳入一個本地 JVM 的 pid, tools.jar 能夠在 jre 目錄下找到。

agent生成

另外,咱們還須要注意 agent 的打包,它須要指定一個 Agent-Class 參數指定咱們的包括 agentmain 方法的類,能夠算是指定入口類吧。

此外,還須要配置 MANIFEST.MF 文件的一些參數,容許咱們從新定義類。若是你的 agent 實現還須要引用一些其餘類庫時,還須要將這些類庫都打包到此 jar 包中,下面是個人 pom 文件配置。

<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <configuration> <archive> <manifestEntries> <Agent-Class>asm.TestAgent</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> <Manifest-Version>1.0</Manifest-Version> <Permissions>all-permissions</Permissions> </manifestEntries> </archive> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> </configuration> </plugin> </plugins> </build> 

另外在打包時須要使用 mvn assembly:assembl 命令生成 jar-with-dependencies 做爲 agent。

代碼實現


我在測試時寫了一個用以上技術實現了一個簡單的字節碼動態修改的 Demo。

被修改的類

TransformTarget 是要被修改的目標類,正常執行時,它會三秒輸出一次 「hello」。

public class TransformTarget { public static void main(String[] args) { while (true) { try { Thread.sleep(3000L); } catch (Exception e) { break; } printSomething(); } } public static void printSomething() { System.out.println("hello"); } } 

Agent

Agent 是執行修改類的主體,它使用 ASM 修改 TransformTarget 類的方法,並使用 instrument 包將修改提交給 JVM。

入口類,也是代理的 Agent-Class。

public class TestAgent { public static void agentmain(String args, Instrumentation inst) { inst.addTransformer(new TestTransformer(), true); try { inst.retransformClasses(TransformTarget.class); System.out.println("Agent Load Done."); } catch (Exception e) { System.out.println("agent load failed!"); } } } 

執行字節碼修改和轉換的類。

public class TestTransformer implements ClassFileTransformer { public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { System.out.println("Transforming " + className); ClassReader reader = new ClassReader(classfileBuffer); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES); ClassVisitor classVisitor = new TestClassVisitor(Opcodes.ASM5, classWriter); reader.accept(classVisitor, ClassReader.SKIP_DEBUG); return classWriter.toByteArray(); } class TestClassVisitor extends ClassVisitor implements Opcodes { TestClassVisitor(int api, ClassVisitor classVisitor) { super(api, classVisitor); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if (name.equals("printSomething")) { mv.visitCode(); Label l0 = new Label(); mv.visitLabel(l0); mv.visitLineNumber(19, l0); mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("bytecode replaced!"); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); Label l1 = new Label(); mv.visitLabel(l1); mv.visitLineNumber(20, l1); mv.visitInsn(Opcodes.RETURN); mv.visitMaxs(2, 0); mv.visitEnd(); TransformTarget.printSomething(); } return mv; } } } 

Attacher

使用 tools.jar 裏方法將 agent 動態加載到目標 JVM 的類。

public class Attacher { public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException { VirtualMachine vm = VirtualMachine.attach("34242"); // 目標 JVM pid vm.loadAgent("/path/to/agent.jar"); } } 

這樣,先啓動 TransformTarget 類,獲取到 pid 後將其傳入 Attacher 裏,並指定 agent jar,將 agent attach 到 TransformTarget 中,原來輸出的 「hello」 就變成咱們想要修改的 「bytecode replaced!」 了。

小結


掌握了字節碼的動態修改技術後,再回頭看 Btrace 的原理就更清晰了,稍微摸索一下咱們也能夠實現一個簡版的。另外不少大牛實現的各類 Java 性能分析工具的技術棧也不外如此,瞭解了這些,將來咱們也能夠寫出適合本身的工具,至少能對別人的工具進行修改~

不得不說 Java 的生態真的很是繁榮,當真是博大精深,查閱一個模塊的資料時能總引出一大堆新的概念,永遠有學不完的新東西。

關於本文有什麼疑問能夠在下面留言交流,若是您以爲本文對您有幫助,歡迎關注個人 微博 或 GitHub 。您也能夠在個人 博客REPO 右上角點擊 Watch 並選擇 Releases only 項來 訂閱 個人博客,有新文章發佈會第一時間通知您。

參考:

教你用Java字節碼作點有趣的事

Java Instrument原理

Java Platform Debugger Architecture Structure Overview

相關文章
相關標籤/搜索