JVM原始碼分析之javaagent原理完全解讀
JVM原始碼分析之javaagent原理完全解讀
概述
本文重點講述javaagent的具體實現,因為它面向的是我們Java程式設計師,而且agent都是用Java編寫的,不需要太多的C/C++程式設計基礎,不過這篇文章裡也會講到JVMTIAgent(C實現的),因為javaagent的執行還是依賴於一個特殊的JVMTIAgent。
對於javaagent,或許大家都聽過,甚至使用過,常見的用法大致如下:
java -javaagent:myagent.jar=mode=test Test
相關廠商內容
關於紅包、SSD雲盤等核心技術集錦!
解析微信朋友圈的lookalike演算法
小邪:阿里8屆雙11容量規劃這樣設計
架構師應該把握這些技術趨勢
效能優化最佳實踐經驗談
我們通過-javaagent來指定我們編寫的agent的jar路徑(./myagent.jar),以及要傳給agent的引數(mode=test),在啟動的時候這個agent就可以做一些我們希望的事了。
javaagent的主要功能如下:
- 可以在載入class檔案之前做攔截,對位元組碼做修改
- 可以在執行期對已載入類的位元組碼做變更,但是這種情況下會有很多的限制,後面會詳細說
- 還有其他一些小眾的功能
- 獲取所有已經載入過的類
- 獲取所有已經初始化過的類(執行過clinit方法,是上面的一個子集)
- 獲取某個物件的大小
- 將某個jar加入到bootstrap classpath裡作為高優先順序被bootstrapClassloader載入
- 將某個jar加入到classpath裡供AppClassloard去載入
- 設定某些native方法的字首,主要在查詢native方法的時候做規則匹配
想象一下可以讓程式按照我們預期的邏輯去執行,聽起來是不是挺酷的。
JVMTI
JVMTI全稱JVM Tool Interface,是JVM暴露出來的一些供使用者擴充套件的介面集合。JVMTI是基於事件驅動的,JVM每執行到一定的邏輯就會呼叫一些事件的回撥介面(如果有的話),這些介面可以供開發者擴充套件自己的邏輯。
比如最常見的,我們想在某個類的位元組碼檔案讀取之後、類定義之前修改相關的位元組碼,從而使建立的class物件是我們修改之後的位元組碼內容,那就可以實現一個回撥函式賦給jvmtiEnv(JVMTI的執行時,通常一個JVMTIAgent對應一個jvmtiEnv,但是也可以對應多個)的回撥方法集合裡的ClassFileLoadHook,這樣在接下來的類檔案載入過程中都會呼叫到這個函式中,大致實現如下:,
jvmtiEventCallbacks callbacks;
jvmtiEnv * jvmtienv = jvmti(agent);
jvmtiError jvmtierror;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;
jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,
&callbacks,
sizeof(callbacks));
JVMTIAgent
JVMTIAgent其實就是一個動態庫,利用JVMTI暴露出來的一些介面來幹一些我們想做、但是正常情況下又做不到的事情,不過為了和普通的動態庫進行區分,它一般會實現如下的一個或者多個函式:
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);
- Agent_OnLoad函式,如果agent是在啟動時載入的,也就是在vm引數裡通過-agentlib來指定的,那在啟動過程中就會去執行這個agent裡的Agent_OnLoad函式。
- Agent_OnAttach函式,如果agent不是在啟動時載入的,而是我們先attach到目標程序上,然後給對應的目標程序傳送load命令來載入,則在載入過程中會呼叫Agent_OnAttach函式。
- Agent_OnUnload函式,在agent解除安裝時呼叫,不過貌似基本上很少實現它。
其實我們每天都在和JVMTIAgent打交道,只是你可能沒有意識到而已,比如我們經常使用Eclipse等工具除錯Java程式碼,其實就是利用JRE自帶的jdwp agent實現的,只是Eclipse等工具在沒讓你察覺的情況下將相關引數(類似-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:61349)自動加到程式啟動引數列表裡了,其中agentlib引數就用來跟要載入的agent的名字,比如這裡的jdwp(不過這不是動態庫的名字,JVM會做一些名稱上的擴充套件,比如在Linux下會去找libjdwp.so的動態庫進行載入,也就是在名字的基礎上加字首lib,再加字尾.so),接下來會跟一堆相關的引數,將這些引數傳給Agent_OnLoad或者Agent_OnAttach函式裡對應的options。
javaagent
說到javaagent,必須要講的是一個叫做instrument的JVMTIAgent(Linux下對應的動態庫是libinstrument.so),因為javaagent功能就是它來實現的,另外instrument agent還有個別名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),這個名字也完全體現了其最本質的功能:就是專門為Java語言編寫的插樁服務提供支援的。
instrument agent
instrument agent實現了Agent_OnLoad和Agent_OnAttach兩方法,也就是說在使用時,agent既可以在啟動時載入,也可以在執行時動態載入。其中啟動時載入還可以通過類似-javaagent:myagent.jar的方式來間接載入instrument agent,執行時動態載入依賴的是JVM的attach機制(JVM Attach機制實現),通過傳送load命令來載入agent。
instrument agent的核心資料結構如下:
struct _JPLISAgent {
JavaVM * mJVM; /* handle to the JVM */
JPLISEnvironment mNormalEnvironment; /* for every thing but retransform stuff */
JPLISEnvironment mRetransformEnvironment;/* for retransform stuff only */
jobject mInstrumentationImpl; /* handle to the Instrumentation instance */
jmethodID mPremainCaller; /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */
jmethodID mAgentmainCaller; /* method on the InstrumentationImpl for agents loaded via attach mechanism */
jmethodID mTransform; /* method on the InstrumentationImpl that does the class file transform */
jboolean mRedefineAvailable; /* cached answer to "does this agent support redefine" */
jboolean mRedefineAdded; /* indicates if can_redefine_classes capability has been added */
jboolean mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */
jboolean mNativeMethodPrefixAdded; /* indicates if can_set_native_method_prefix capability has been added */
char const * mAgentClassName; /* agent class name */
char const * mOptionsString; /* -javaagent options string */
};
struct _JPLISEnvironment {
jvmtiEnv * mJVMTIEnv; /* the JVM TI environment */
JPLISAgent * mAgent; /* corresponding agent */
jboolean mIsRetransformer; /* indicates if special environment */
};
這裡解釋一下幾個重要項:
- mNormalEnvironment:主要提供正常的類transform及redefine功能。
- mRetransformEnvironment:主要提供類retransform功能。
- mInstrumentationImpl:這個物件非常重要,也是我們Java agent和JVM進行互動的入口,或許寫過javaagent的人在寫`premain`以及`agentmain`方法的時候注意到了有個Instrumentation引數,該引數其實就是這裡的物件。
- mPremainCaller:指向`sun.instrument.InstrumentationImpl.loadClassAndCallPremain`方法,如果agent是在啟動時載入的,則該方法會被呼叫。
- mAgentmainCaller:指向`sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain`方法,該方法在通過attach的方式動態載入agent的時候呼叫。
- mTransform:指向`sun.instrument.InstrumentationImpl.transform`方法。
- mAgentClassName:在我們javaagent的MANIFEST.MF裡指定的`Agent-Class`。
- mOptionsString:傳給agent的一些引數。
- mRedefineAvailable:是否開啟了redefine功能,在javaagent的MANIFEST.MF裡設定`Can-Redefine-Classes:true`。
- mNativeMethodPrefixAvailable:是否支援native方法字首設定,同樣在javaagent的MANIFEST.MF裡設定`Can-Set-Native-Method-Prefix:true`。
- mIsRetransformer:如果在javaagent的MANIFEST.MF檔案裡定義了`Can-Retransform-Classes:true`,將會設定mRetransformEnvironment的mIsRetransformer為true。
在啟動時載入instrument agent
正如前面“概述”裡提到的方式,就是啟動時載入instrument agent,具體過程都在`InvocationAdapter.c`的`Agent_OnLoad`方法裡,這裡簡單描述下過程:
- 建立並初始化JPLISAgent
- 監聽VMInit事件,在vm初始化完成之後做下面的事情:
- 建立InstrumentationImpl物件
- 監聽ClassFileLoadHook事件
- 呼叫InstrumentationImpl的`loadClassAndCallPremain`方法,在這個方法裡會呼叫javaagent裡MANIFEST.MF裡指定的`Premain-Class`類的premain方法
- 解析javaagent裡MANIFEST.MF裡的引數,並根據這些引數來設定JPLISAgent裡的一些內容
在執行時載入instrument agent
在執行時載入的方式,大致按照下面的方式來操作:
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentPath, agentArgs);
上面會通過JVM的attach機制來請求目標JVM載入對應的agent,過程大致如下:
- 建立並初始化JPLISAgent
- 解析javaagent裡MANIFEST.MF裡的引數
- 建立InstrumentationImpl物件
- 監聽ClassFileLoadHook事件
- 呼叫InstrumentationImpl的loadClassAndCallAgentmain方法,在這個方法裡會呼叫javaagent裡MANIFEST.MF裡指定的Agent-Class類的agentmain方法
instrument agent的ClassFileLoadHook回撥實現
不管是啟動時還是執行時載入的instrument agent,都關注著同一個jvmti事件——ClassFileLoadHook,這個事件是在讀取位元組碼檔案之後回撥時用的,這樣可以對原來的位元組碼做修改,那這裡面究竟是怎樣實現的呢?
void JNICALL
eventHandlerClassFileLoadHook( jvmtiEnv * jvmtienv,
JNIEnv * jnienv,
jclass class_being_redefined,
jobject loader,
const char* name,
jobject protectionDomain,
jint class_data_len,
const unsigned char* class_data,
jint* new_class_data_len,
unsigned char** new_class_data) {
JPLISEnvironment * environment = NULL;
environment = getJPLISEnvironment(jvmtienv);
/* if something is internally inconsistent (no agent), just silently return without touching the buffer */
if ( environment != NULL ) {
jthrowable outstandingException = preserveThrowable(jnienv);
transformClassFile( environment->mAgent,
jnienv,
loader,
name,
class_being_redefined,
protectionDomain,
class_data_len,
class_data,
new_class_data_len,
new_class_data,
environment->mIsRetransformer);
restoreThrowable(jnienv, outstandingException);
}
}
先根據jvmtiEnv取得對應的JPLISEnvironment,因為上面我已經說到其實有兩個JPLISEnvironment(並且有兩個jvmtiEnv),其中一個是專門做retransform的,而另外一個用來做其他事情,根據不同的用途,在註冊具體的ClassFileTransformer時也是分開的,對於作為retransform用的ClassFileTransformer,我們會註冊到一個單獨的TransformerManager裡。
接著呼叫transformClassFile方法,由於函式實現比較長,這裡就不貼程式碼了,大致意思就是呼叫InstrumentationImpl物件的transform方法,根據最後那個引數來決定選哪個TransformerManager裡的ClassFileTransformer物件們做transform操作。
private byte[]
transform( ClassLoader loader,
String classname,
Class classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer,
boolean isRetransformer) {
TransformerManager mgr = isRetransformer?
mRetransfomableTransformerManager :
mTransformerManager;
if (mgr == null) {
return null; // no manager, no transform
} else {
return mgr.transform( loader,
classname,
classBeingRedefined,
protectionDomain,
classfileBuffer);
}
}
public byte[]
transform( ClassLoader loader,
String classname,
Class classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer) {
boolean someoneTouchedTheBytecode = false;
TransformerInfo[] transformerList = getSnapshotTransformerList();
byte[] bufferToUse = classfileBuffer;
// order matters, gotta run 'em in the order they were added
for ( int x = 0; x < transformerList.length; x++ ) {
TransformerInfo transformerInfo = transformerList[x];
ClassFileTransformer transformer = transformerInfo.transformer();
byte[] transformedBytes = null;
try {
transformedBytes = transformer.transform( loader,
classname,
classBeingRedefined,
protectionDomain,
bufferToUse);
}
catch (Throwable t) {
// don't let any one transformer mess it up for the others.
// This is where we need to put some logging. What should go here? FIXME
}
if ( transformedBytes != null ) {
someoneTouchedTheBytecode = true;
bufferToUse = transformedBytes;
}
}
// if someone modified it, return the modified buffer.
// otherwise return null to mean "no transforms occurred"
byte [] result;
if ( someoneTouchedTheBytecode ) {
result = bufferToUse;
}
else {
result = null;
}
return result;
}
以上是最終調到的java程式碼,可以看到已經呼叫到我們自己編寫的javaagent程式碼裡了,我們一般是實現一個ClassFileTransformer類,然後建立一個物件註冊到對應的TransformerManager裡。
Class Transform的實現
這裡說的class transform其實是狹義的,主要是針對第一次類檔案載入時就要求被transform的場景,在載入類檔案的時候發出ClassFileLoad事件,然後交給instrumenat agent來呼叫javaagent裡註冊的ClassFileTransformer實現位元組碼的修改。
Class Redefine的實現
類重新定義,這是Instrumentation提供的基礎功能之一,主要用在已經被載入過的類上,想對其進行修改,要做這件事,我們必須要知道兩個東西,一個是要修改哪個類,另外一個是想將那個類修改成怎樣的結構,有了這兩個資訊之後就可以通過InstrumentationImpl下面的redefineClasses方法操作了:
public void redefineClasses(ClassDefinition[] definitions) throws ClassNotFoundException {
if (!isRedefineClassesSupported()) {
throw new UnsupportedOperationException("redefineClasses is not supported in this environment");
}
if (definitions == null) {
throw new NullPointerException("null passed as 'definitions' in redefineClasses");
}
for (int i = 0; i < definitions.length; ++i) {
if (definitions[i] == null) {
throw new NullPointerException("element of 'definitions' is null in redefineClasses");
}
}
if (definitions.length == 0) {
return; // short-circuit if there are no changes requested
}
redefineClasses0(mNativeAgent, definitions);
}
在JVM裡對應的實現是建立一個VM_RedefineClasses的VM_Operation,注意執行它的時候會stop-the-world:
jvmtiError
JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) {
//TODO: add locking
VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);
VMThread::execute(&op);
return (op.check_error());
} /* end RedefineClasses */
這個過程我儘量用語言來描述清楚,不詳細貼程式碼了,因為程式碼量實在有點大:
- 挨個遍歷要批量重定義的jvmtiClassDefinition
- 然後讀取新的位元組碼,如果有關注ClassFileLoadHook事件的,還會走對應的transform來對新的位元組碼再做修改
- 位元組碼解析好,建立一個klassOop物件
- 對比新老類,並要求如下:
- 父類是同一個
- 實現的介面數也要相同,並且是相同的介面
- 類訪問符必須一致
- 欄位數和欄位名要一致
- 新增的方法必須是private static/final的
- 可以刪除修改方法
- 對新類做位元組碼校驗
- 合併新老類的常量池
- 如果老類上有斷點,那都清除掉
- 對老類做JIT去優化
- 對新老方法匹配的方法的jmethodId做更新,將老的jmethodId更新到新的method上
- 新類的常量池的holer指向老的類
- 將新類和老類的一些屬性做交換,比如常量池,methods,內部類
- 初始化新的vtable和itable
- 交換annotation的method、field、paramenter
- 遍歷所有當前類的子類,修改他們的vtable及itable
上面是基本的過程,總的來說就是隻更新了類裡的內容,相當於只更新了指標指向的內容,並沒有更新指標,避免了遍歷大量已有類物件對它們進行更新所帶來的開銷。
Class Retransform的實現
retransform class可以簡單理解為回滾操作,具體回滾到哪個版本,這個需要看情況而定,下面不管那種情況都有一個前提,那就是javaagent已經要求要有retransform的能力了:
- 如果類是在第一次載入的的時候就做了transform,那麼做retransform的時候會將程式碼回滾到transform之後的程式碼
- 如果類是在第一次載入的的時候沒有任何變化,那麼做retransform的時候會將程式碼回滾到最原始的類檔案裡的位元組碼
- 如果類已經載入了,期間類可能做過多次redefine(比如被另外一個agent做過),但是接下來載入一個新的agent要求有retransform的能力了,然後對類做redefine的動作,那麼retransform的時候會將程式碼回滾到上一個agent最後一次做redefine後的位元組碼
我們從InstrumentationImpl的retransformClasses方法引數看猜到應該是做回滾操作,因為我們只指定了class:
public void retransformClasses(Class<?>[] classes) {
if (!isRetransformClassesSupported()) {
throw new UnsupportedOperationException( "retransformClasses is not supported in this environment");
}
retransformClasses0(mNativeAgent, classes);
}
不過retransform的實現其實也是通過redefine的功能來實現,在類載入的時候有比較小的差別,主要體現在究竟會走哪些transform上,如果當前是做retransform的話,那將忽略那些註冊到正常的TransformerManager裡的ClassFileTransformer,而只會走專門為retransform而準備的TransformerManager的ClassFileTransformer,不然想象一下位元組碼又被無聲無息改成某個中間態了。
private:
void post_all_envs() {
if (_load_kind != jvmti_class_load_kind_retransform) {
// for class load and redefine,
// call the non-retransformable agents
JvmtiEnvIterator it;
for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {
if (!env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) {
// non-retransformable agents cannot retransform back,
// so no need to cache the original class file bytes
post_to_env(env, false);
}
}
}
JvmtiEnvIterator it;
for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {
// retransformable agents get all events
if (env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) {
// retransformable agents need to cache the original class file
// bytes if changes are made via the ClassFileLoadHook
post_to_env(env, true);
}
}
}
javaagent的其他小眾功能
javaagent除了做位元組碼上面的修改之外,其實還有一些小功能,有時候還是挺有用的
- 獲取所有已經被載入的類:Class[] getAllLoadedClasses();
- 獲取所有已經初始化了的類: Class[] getInitiatedClasses(ClassLoader loader);
- 獲取某個物件的大小: long getObjectSize(Object objectToSize);
- 將某個jar加入到bootstrap classpath裡優先其他jar被載入: void appendToBootstrapClassLoaderSearch(JarFile jarfile);
- 將某個jar加入到classpath裡供appclassloard去載入:void appendToSystemClassLoaderSearch(JarFile jarfile);
- 設定某些native方法的字首,主要在找native方法的時候做規則匹配: void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)。
JavaAgent 是JDK 1.5 以後引入的,也可以叫做Java代理。
JavaAgent 是執行在 main方法之前的攔截器,它內定的方法名叫 premain ,也就是說先執行 premain 方法然後再執行 main 方法。
那麼如何實現一個 JavaAgent 呢?很簡單,只需要增加 premain 方法即可。
看下面的程式碼和程式碼中的註釋說明:
-
package com.shanhy.demo.agent;
-
import java.lang.instrument.Instrumentation;
-
/**
-
* 我的Java代理
-
*
-
* @author 單紅宇(365384722)
-
* @myblog http://blog.csdn.net/catoop/
-
* @create 2016年3月30日
-
*/
-
public class MyAgent {
-
/**
-
* 該方法在main方法之前執行,與main方法執行在同一個JVM中
-
* 並被同一個System ClassLoader裝載
-
* 被統一的安全策略(security policy)和上下文(context)管理
-
*
-
* @param agentOps
-
* @param inst
-
* @author SHANHY
-
* @create 2016年3月30日
-
*/
-
public static void premain(String agentOps, Instrumentation inst) {
-
System.out.println("=========premain方法執行========");
-
System.out.println(agentOps);
-
}
-
/**
-
* 如果不存在 premain(String agentOps, Instrumentation inst)
-
* 則會執行 premain(String agentOps)
-
*
-
* @param agentOps
-
* @author SHANHY
-
* @create 2016年3月30日
-
*/
-
public static void premain(String agentOps) {
-
System.out.println("=========premain方法執行2========");
-
System.out.println(agentOps);
-
}
-
}
寫完這個類後,我們還需要做一步配置工作。
在 src 目錄下新增 META-INF/MANIFEST.MF 檔案,內容按如下定義:
-
Manifest-Version: 1.0
-
Premain-Class: com.shanhy.demo.agent.MyAgent
-
Can-Redefine-Classes: true
-
Can-Retransform-Classes: true
-
Can-Set-Native-Method-Prefix: false
例如:
-
Manifest-Version: 1.0 Created-By: Apache Maven 3.5.4 Built-By: jerry Build-Jdk: 11.0.1 Boot-Class-Path: transmittable-thread-local-2.10.2.jar Can-Redefine-Classes: false Can-Retransform-Classes: true Can-Set-Native-Method-Prefix: false Premain-Class: com.alibaba.ttl.threadpool.agent.Tt