本課時我們主要分析如何使用 Java Agent 技術對字節碼進行修改。
Java 5 版本以后,JDK 有一個包叫做 instrument ,能夠實現一些非常酷的功能,市面上一些 APM 工具,就是通過它來進行的增強,這個功能對于業務開發者來說,是比較偏門的。但你可能在無意中已經用到它了,比如 Jrebel 酷炫的熱部署功能(這個工具能夠顯著增加開發效率)。
本課時將以一個例子來看一下具體的應用場景,然后介紹一個在線上常用的問題排查工具:Arthas。
#### Java Agent 介紹
我們上面說的這些工具的基礎,就是 Java Agent 技術,可以利用它來構建一個附加的代理程序,用來協助檢測性能,還可以替換一些現有功能,甚至 JDK 的一些類我們也能修改,有點像 JVM 級別的 AOP 功能。
通常,Java 入口是一個 main 方法,這是毋庸置疑的,而 Java Agent 的入口方法叫做 premain,表明是在 main 運行之前的一些操作。Java Agent 就是這樣的一個 jar 包,定義了一個標準的入口方法,它并不需要繼承或者實現任何其他的類,屬于無侵入的一種開發模式。
> 為什么叫 premain?這是一個約定,并沒有什么其他的理由,這個方法,無論是第一次加載,還是每次新的 ClassLoader 加載,都會執行。
我們可以在這個前置的方法里,對字節碼進行一些修改,來增加功能或者改變代碼的行為,這種方法沒有侵入性,只需要在啟動命令中加上 -javaagent 參數就可以了。Java 6 以后,甚至可以通過 attach 的方式,動態的給運行中的程序設置加載代理類。
其實,instrument 一共有兩個 main 方法,一個是 premain,另一個是 agentmain,但在一個 JVM 中,只會調用一個;前者是 main 執行之前的修改,后者是控制類運行時的行為。它們還是有一些區別的,agentmain 因為能夠動態修改大部分代碼,比較危險,限制會更大一些。
#### 有什么用
* [ ] 獲取統計信息
在許多 APM 產品里,比如 Pinpoint、SkyWalking 等,就是使用 Java Agent 對代碼進行的增強。通過在方法執行前后動態加入的統計代碼,來進行監控信息的收集;通過兼容 OpenTracing 協議,可以實現分布式鏈路追蹤的功能。
它的原理類似于 AOP,最終以字節碼的形式存在,性能損失取決于你的代碼邏輯。
* [ ] 熱部署
通過自定義的 ClassLoader,可以實現代碼的熱替換。使用 agentmain,實現熱部署功能會更加便捷,通過 agentmain 獲取到 Instrumentation 以后,就可以對類進行動態重定義了。
* [ ] 診斷
配合 JVMTI 技術,可以 attach 到某個進程進行運行時的統計和調試,比較流行的 btrace 和 arthas ,其底層就是這種技術。
* [ ] 代碼示例
要構建一個 agent 程序,大體可分為以下步驟:
* 使用字節碼增強工具,編寫增強代碼;
* 在 manifest 中指定 Premain-Class/Agent-Class 屬性;
* 使用參數加載或者使用 attach 方式。
我們來詳細介紹一下這個過程。
#### 編寫 Agent
Java Agent 最終的體現方式是一個 jar 包,使用 IDEA 創建一個默認的 maven 工程即可。
創建一個普通的 Java 類,添加 premain 或者 agentmain 方法,它們的參數完全一樣。

#### 編寫 Transformer
實際的代碼邏輯需要實現 ClassFileTransformer 接口。假如我們要統計某個方法的執行時間,使用 JavaAssist 工具來增強字節碼,則可以通過以下代碼來實現:
* 獲取 MainRun 類的字節碼實例;
* 獲取 hello 方法的字節碼實例;
* 在方法前后,加入時間統計,首先定義變量 _begin,然后追加要寫的代碼。
別忘了加入 maven 依賴,我們借用 javassist 完成字節碼增強:
```
<dependency>
????<groupId>org.javassist</groupId>
????<artifactId>javassist</artifactId>
????<version>3.24.1-GA</version>
</dependency>
```

字節碼增強也可以使用 Cglib、ASM 等其他工具。
* [ ] MANIFEST.MF 文件
那么我們編寫的代碼是如何讓外界知曉的呢?那就是依靠 MANIFEST.MF 文件,具體路徑在
src/main/resources/META-INF/MANIFEST.MF:
```
Manifest-Version:?1.0
premain-class:?com.sayhiai.example.javaagent.AgentApp
```
一般的,maven 打包會覆蓋這個文件,所以我們需要為它指定一個。
```
<build><plugins><plugin>
<groupId>org.apache.maven.plugins</groupId>
????<artifactId>maven-jar-plugin</artifactId>
????<configuration>
????????<archive>
????????????<manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile>
????????????</archive>
????</configuration></plugin></plugins></build>
```
然后,在命令行,執行 mvn install 安裝到本地代碼庫,或者使用 mvn deploy 發布到私服上。
附 MANIFEST.MF 參數清單:
```
Premain-Class
Agent-Class
Boot-Class-Path
Can-Redefine-Classes
Can-Retransform-Classes
Can-Set-Native-Method-Prefix
```
* [ ] 使用
使用方式取決于你使用的 premain 還是 agentmain,它們之間有一些區別,具體如下。
* [ ] premain
在我們的例子中,直接在啟動命令行中加入參數即可,在 jvm 啟動時啟用代理。
```
java?-javaagent:agent.jar?MainRun
```
在 IDEA 中,可以將參數附著在 jvm options 里。

接下來看一下測試代碼。

這是我們的執行類,執行后,直接輸出 hello world。通過增強以后,還額外的輸出了執行時間,以及一些 debug 信息。其中,debug 信息在 main 方法執行之前輸出。

* [ ] agentmain
這種模式一般用在一些診斷工具上。使用 jdk/lib/tools.jar 中的工具類,可以動態的為運行中的程序加入一些功能。它的主要運行步驟如下:
* 獲取機器上運行的所有 JVM 進程 ID;
* 選擇要診斷的 jvm;
* 將 jvm 使用 attach 函數鏈接上;
* 使用 loadAgent 函數加載 agent,動態修改字節碼;
* 卸載 jvm。
代碼樣例如下:
```
import?com.sun.tools.attach.VirtualMachine;
import?com.sun.tools.attach.VirtualMachineDescriptor;
import?java.util.List;
public?class?JvmAttach?{
????public?static?void?main(String[]?args)
????????????throws?Exception?{
????????List<VirtualMachineDescriptor>?list?=?VirtualMachine.list();
????????for?(VirtualMachineDescriptor?vmd?:?list)?{
????????????if?(vmd.displayName().endsWith("MainRun"))?{
????????????????VirtualMachine?virtualMachine?=?VirtualMachine.attach(vmd.id());
????????????????virtualMachine.loadAgent("test.jar?",?"...");
????????????????//.....
????????????????virtualMachine.detach();
????????????}
????????}
????}
```
這些代碼功能雖然強大,但都是比較危險的,這就是為什么 Btrace 說了這么多年,還是只在小范圍內被小心的使用。相對來說,Arthas 顯的友好而且安全的多。
使用注意點
(1)jar 包依賴方式
一般,Agent 的 jar 包會以 fatjar 的方式提供,即將所有的依賴打包到一個大的 jar 包中。如果你的功能復雜、依賴多,那么這個 jar 包將會特別的大。
使用獨立的 bom 文件維護這些依賴是另外一種方法。使用方自行管理依賴問題,但這通常會發生一些找不到 jar 包的錯誤,更糟糕的是,大多數在運行時才發現。
(2)類名稱重復
不要使用和 jdk 及 instrument 包中相同的類名(包括包名),有時候你能夠僥幸過關,但也會陷入無法控制的異常中。
(3)做有限的功能
可以看到,給系統動態的增加功能是非常酷的,但大多數情況下非常耗費性能。你會發現,一些簡單的診斷工具,會占用你 1 核的 CPU,這是很平常的事情。
(4)ClassLoader
如果你用的 JVM 比較舊,頻繁地生成大量的代理類,會造成元空間的膨脹,容易發生內存占用問題。
ClassLoader 有雙親委派機制,如果你想要替換相應的類,一定要搞清楚它的類加載器應該用哪個,否則替換的類,是不生效的。
具體的調試方法,可以在 Java 進程啟動時,加入 -verbose:class 參數,用來監視引用程序對類的加載。
#### Arthas
我們來回顧一下在故障排查時所做的一些準備和工具支持。
在第 09 課時,我們了解了 jstat 工具,還有 jmap 等查看內存狀態的工具;第 11 課時,介紹了超過 20 個工具的使用,這需要開發和分析的人員具有較高的水平;第 15 課時,還介紹了 jstack 的一些典型狀態。對于這種瞬時態問題的分析,需要綜合很多工具,對剛進入這個行業的人來說,很不友好。
Arthas 就是使用 Java Agent 技術編寫的一個工具,具體采用的方式,就是我們上面提到的 attach 方式,它會無侵入的 attach 到具體的執行進程上,方便進行問題分析。
你甚至可以像 debug 本地的 Java 代碼一樣,觀測到方法執行的參數值,甚至做一些統計分析。這通常可以解決下面的問題:
* 哪個線程使用了最多的 CPU
* 運行中是否有死鎖,是否有阻塞
* 如何監測一個方法哪里耗時最高
* 追加打印一些 debug 信息
* 監測 JVM 的實時運行狀態
[Arthas 官方文檔十分詳細,也可以點擊這里參考](https://alibaba.github.io/arthas)。
但無論工具如何強大,一些基礎知識是需要牢固掌握的,否則,工具中出現的那些術語,也會讓人一頭霧水。
工具常變,但基礎更加重要。如果你想要一個適應性更強的技術棧,還是要多花點時間在原始的排查方法上。
#### 小結
本課時介紹了開發人員極少接觸的 Java Agent 技術,但在平常的工作中你可能不知不覺就用到它了。在平常的面試中,一些面試官也會經常問一些相關的問題,以此來判斷你對整個 Java 體系的掌握程度,如果你能回答上來,說明你已經脫穎而出了。
值得注意的是,這個知識點,對于做基礎架構(比如中間件研發)的人來說,是必備技能,如果不了解,那面試可能就要涼了。
從實用角度來說,阿里開源的 Arthas 工具,是非常好用的,如果你有線上的運維權限,不妨嘗試一下。
[本課時項目代碼,可點擊這里查看](https://gitee.com/xjjdog/jvm-lagou-res/tree/master/jvm-21/javaagent-demo)。
- 前言
- 開篇詞
- 基礎原理
- 第01講:一探究竟:為什么需要 JVM?它處在什么位置?
- 第02講:大廠面試題:你不得不掌握的 JVM 內存管理
- 第03講:大廠面試題:從覆蓋 JDK 的類開始掌握類的加載機制
- 第04講:動手實踐:從棧幀看字節碼是如何在 JVM 中進行流轉的
- 垃圾回收
- 第05講:大廠面試題:得心應手應對 OOM 的疑難雜癥
- 第06講:深入剖析:垃圾回收你真的了解嗎?(上)
- 第06講:深入剖析:垃圾回收你真的了解嗎?(下)
- 第07講:大廠面試題:有了 G1 還需要其他垃圾回收器嗎?
- 第08講:案例實戰:億級流量高并發下如何進行估算和調優
- 實戰部分
- 第09講:案例實戰:面對突如其來的 GC 問題如何下手解決
- 第10講:動手實踐:自己模擬 JVM 內存溢出場景
- 第11講:動手實踐:遇到問題不要慌,輕松搞定內存泄漏
- 第12講:工具進階:如何利用 MAT 找到問題發生的根本原因
- 第13講:動手實踐:讓面試官刮目相看的堆外內存排查
- 第14講:預警與解決:深入淺出 GC 監控與調優
- 第15講:案例分析:一個高死亡率的報表系統的優化之路
- 第16講:案例分析:分庫分表后,我的應用崩潰了
- 進階部分
- 第17講:動手實踐:從字節碼看方法調用的底層實現
- 第18講:大廠面試題:不要搞混 JMM 與 JVM
- 第19講:動手實踐:從字節碼看并發編程的底層實現
- 第20講:動手實踐:不為人熟知的字節碼指令
- 第21講:深入剖析:如何使用 Java Agent 技術對字節碼進行修改
- 第22講:動手實踐:JIT 參數配置如何影響程序運行?
- 第23講:案例分析:大型項目如何進行性能瓶頸調優?
- 彩蛋
- 第24講:未來:JVM 的歷史與展望
- 第25講:福利:常見 JVM 面試題補充