冷門instrument包,功能d炸天(java進階)

冷門instrument包,功能d炸天(java進階)

文中代碼示例工程如下,更多參考btrace和arthas:

https://github.com/sayhiai/example-javaagent

5版本以後,jdk有一個包叫做instrument,能夠實現一些非常酷的功能。市面上一些APM工具,就是通過它來進行的增強。

這是基礎架構的必備技能,但對業務開發來說並不是。許多面試會問到這個知識點,並不是因為將來會用到,而是因為你說對jdk比較熟悉,他想殺殺你的威風。

不會用沒問題,但你要說不知道,就過分了點。

javaagent介紹

我們通常的java入口都是一個main方法,而javaagent的入口方法叫做premain,表明是在main運行之前的一些操作。javaagent就是一個jar包,定義了一個標準的premain()方法,並不需要繼承或者實現任何其他的類。

這是一個約定,並木有什麼其他的理由。這個方法,無論是第一次加載,還是每次新的ClassLoader加載,都會執行。

我們可以在這個前置的方法裡,對字節碼進行一些修改,來增加功能或者改變代碼的行為。這種方法沒有侵入性,只需要在啟動命令中加上-javaagent參數就可以。Java6以後,甚至可以通過attach的方式,動態的給運行中的程序設置加載代理類。

有經驗的同學肯定要提出異議了。其實,instrument有兩個main方法,一個是premain,一個是agentmain,在一個JVM中,只會調用一個;前者是main執行之前的修改,後者控制類運行時的行為。它們還是有一些區別的,agentmain因為比較危險,限制會更大一些。

有什麼用

獲取統計信息

許多apm產品,比如Pinpoint、SkyWalking等,就是使用javaagent對代碼進行的增強。通過在方法執行前後動態加入的統計代碼,進行監控信息的收集;通過兼容OpenTracing協議,可以實現分佈式鏈路追蹤的功能。

它的原理類似於aop,最終以字節碼存在,性能損失取決於你的代碼邏輯。

熱部署

通過自定義的ClassLoader,可以實現代碼的熱替換。使用agentmain,實現熱部署功能會更加便捷。通過agentmain獲取到Instrumentation以後,就可以對類進行動態重定義。

診斷

配合JVMTI技術,可以attach到某個進程進行運行時統計和調試,比較流行的btrace和arthas,底層就是這種技術。

如何做

大體分為以下步驟:

  • 構建agent jar包,編寫增強代碼
  • 在manifest中指定Premain-Class/Agent-Class屬性
  • 使用參數加載或者attach方式使用

編寫Agent

javaagent最終的體現方式是一個jar包。使用idea創建一個默認的maven工程即可。

創建一個普通java類,添加premain或者agentmain方法,它們的參數完全一樣。

冷門instrument包,功能d炸天(java進階)

編寫Transformer

此部分,要藉助額外jar包的功能。

實際的代碼邏輯需要實現ClassFileTransformer接口。假如我們要統計某個方法的執行時間。我們使用javaassist來增強字節碼,則可以通過以下代碼來實現。

  • 獲取MainRun類的字節碼實例
  • 獲取hello方法的字節碼實例
  • 在方法前後,加入時間統計,首先定義變量_begin,然後直接編寫代碼

別忘了加入maven依賴

<dependency>
<groupid>org.javassist/<groupid>
<artifactid>javassist/<artifactid>
<version>3.24.1-GA/<version>
/<dependency>
冷門instrument包,功能d炸天(java進階)

字節碼增強也可以使用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裡。

冷門instrument包,功能d炸天(java進階)

接下來看一下測試代碼。

冷門instrument包,功能d炸天(java進階)

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

冷門instrument包,功能d炸天(java進階)

agentmain

一般用在一些診斷工具上。使用jdk/lib/tools.jar中的功能,可以動態的為運行中的程序加入功能。主要有以下步驟:

  • 獲取機器上運行的所有jvm的進程id
  • 選擇要診斷的jvm
  • 將jvm使用attach函數鏈接上
  • 使用loadAgent函數加載agent,動態修改字節碼
  • 卸載jvm
冷門instrument包,功能d炸天(java進階)

這些代碼都是比較危險的,這就是為什麼Btrace說了這麼多年,還是隻在小範圍內被小心使用。相對來說,arthas顯的友好而且安全的多。

注意點

一、jar包依賴方式

一般,agent的jar包會以fatjar的方式提供,即將所有的依賴打包到一個大的jar包中。

如果你的功能複雜,依賴多,那麼這個jar包將會特別的大。

使用獨立的bom文件維護這些依賴是另外一種方法。使用方自行管理依賴問題,但這通常會發生一些找不到jar包的錯誤。更糟糕的是,大多數在運行時才發現。

二、類名稱重複

不要使用和jdk以及instrument包中相同的類名(包括包名),有時候你能夠僥倖過關,但也會陷入無法控制的異常中。

三、做有限的功能

可以看到,給系統動態的增加功能是非常酷的,但大多數情況下非常耗費性能。你會發現,一些簡單的診斷工具,佔用你1核的cpu,是稀鬆平常的事情。

四、ClassLoader

如果你用的jvm比較舊,頻繁的生成大量的代理類,會造成perm區的膨脹,容易發生OOM。

ClassLoader有雙親委派機制,如果你想要替換相應的類,一定要搞清楚它的類加載器應該用哪個。否則替換的類,是不生效的哦。

End

將你的增強代碼,加入類似zk的主動通知功能,可以通過管理後臺動態的調整應用的行為。如果再集成一個類似groovy的腳本語言,理論上,你能夠幹任何事情。

冷門instrument包,功能d炸天(java進階)

所以,使用-javaagent參數引入的jar包,或者使用attach方式提供的一些診斷工具,小姐姐都不敢隨便的用。

作者簡介:小姐姐味道 (xjjdog),一個不允許程序員走彎路的公眾號。聚焦基礎架構和Linux。十年架構,日百億流量,與你探討高併發世界,給你不一樣的味道。我的個人微信xjjdog0,歡迎添加好友,​進一步交流。​


分享到:


相關文章: