簡介
VMTI(JVM Tool Interface)是 Java 虛擬機所提供的 native 編程接口擂达,是 JVMPI(Java Virtual Machine Profiler Interface)
Agent 的工作過程
Agent 是在 Java 虛擬機啟動之時加載的赠橙,這個加載處于虛擬機初始化的早期馍忽,在這個時間點上:
所有的 Java 類都未被初始化赐劣;
所有的對象實例都未被創(chuàng)建萝究;
因而,沒有任何 Java 代碼被執(zhí)行晒喷;
但在這個時候,我們已經(jīng)可以:
操作 JVMTI 的 Capability 參數(shù)凉敲;
使用系統(tǒng)參數(shù);
動態(tài)庫被加載之后果复,虛擬機會先尋找一個 Agent 入口函數(shù):
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
這個函數(shù)中独柑,虛擬機傳入了一個 JavaVM 指針车酣,以及命令行的參數(shù)者春。
通過 JavaVM,我們可以獲得 JVMTI 的指針,并獲得 JVMTI 函數(shù)的使用能力拥刻,所有的 JVMTI 函數(shù)都通過這個 jvmtiEnv 獲取,不同的虛擬機實現(xiàn)提供的函數(shù)細節(jié)可能不一樣,但是使用的方式是統(tǒng)一的霜运。
jvmtiEnv *jvmti;
...
(*jvm)->GetEnv(jvm, &jvmti, JVMTI_VERSION_1_0);
JVMTI的啟動方式
JVMTI有兩種啟動方式池摧,第一種是隨java進程啟動時,自動載入共享庫。另一種方式是蹈丸,java運行時荸百,通過attach api動態(tài)載入光绕。
方式1的實現(xiàn)方式是通過在java啟動時傳遞一個特殊的option:
java -agentlib:<agent-lib-name>=<options> Sample
注意停蕉,這里的共享庫路徑是環(huán)境變量路徑驯绎,例如 java -agentlib:foo=opt1,opt2册着,java啟動時會從linux的LD_LIBRARY_PATH或windows的PATH環(huán)境變量定義的路徑處裝載foo.so或foo.dll拴孤,找不到則拋異常
java -agentpath:<path-to-agent>=<options> Sample
這是以絕對路徑的方式裝載共享庫,例如 java -agentpath:/home/admin/agentlib/foo.so=opt1,opt2
windows下:的動態(tài)鏈接生成為dll而不是.so文件,稍后主要使用windows下vs編譯共享庫文件
方式2的實現(xiàn)方式是通過attach api司顿,這是一套純java的api,它負責(zé)動態(tài)地將dynamic module attach到指定進程id的java進程內(nèi)并觸發(fā)回調(diào):
import java.io.IOException;
import com.sun.tools.attach.VirtualMachine;
public class VMAttacher {
public static void main(String[] args) throws Exception {
// args[0]為java進程id
VirtualMachine virtualMachine = com.sun.tools.attach.VirtualMachine.attach(args[0]);
// args[1]為共享庫路徑朦拖,args[2]為傳遞給agent的參數(shù)
virtualMachine.loadAgentPath(args[1], args[2]);
virtualMachine.detach();
}
}
- Attach API位于$JAVA_HOME/lib/tools.jar,所以在編譯時作喘,需要將這個jar放入classpath
javac -cp $JAVA_HOME/lib/tools.jar VMAttacher.java
簡單開發(fā)JVMTI Agent
下面一個簡單的例子理疙,闡述如何開發(fā)一個簡單的 Agent 砖顷。這個 Agent 是通過 C++ 編寫的贰锁,通過監(jiān)聽 JVMTI_EVENT_METHOD_ENTRY 事件赃梧,
注冊對應(yīng)的回調(diào)函數(shù)來響應(yīng)這個事件,來輸出所有被調(diào)用函數(shù)名豌熄。
Agent.cpp
#include <iostream>
#include "MethodTraceAgent.h"
#include <jvmti.h>
using namespace std;
JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *vm, char *options, void *reserved)
{
cout << "Agent_OnLoad(" << vm << ")" << endl;
try{
MethodTraceAgent* agent = new MethodTraceAgent();
agent->Init(vm);
agent->ParseOptions(options);
agent->AddCapability();
agent->RegisterEvent();
} catch (AgentException& e) {
cout << "Error when enter HandleMethodEntry: " << e.what() << " [" << e.ErrCode() << "]";
return JNI_ERR;
}
return JNI_OK;
}
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm)
{
cout << "Agent_OnUnload(" << vm << ")" << endl;
}
MethodTraceAgent.h
#include "<jvmti.h>"
#include <string>
using namespace std;
class AgentException
{
public:
AgentException(jvmtiError err) {
m_error = err;
}
string what() const throw() {
return "AgentException";
}
jvmtiError ErrCode() const throw() {
return m_error;
}
private:
jvmtiError m_error;
};
class MethodTraceAgent
{
public:
MethodTraceAgent() {}
~MethodTraceAgent() ;
void Init(JavaVM *vm) ;
void ParseOptions(const char* str) ;
void AddCapability() ;
void RegisterEvent() ;
static void JNICALL HandleMethodEntry(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jmethodID method);
private:
static void CheckException(jvmtiError error)
{
// 可以根據(jù)錯誤類型擴展對應(yīng)的異常授嘀,這里只做簡單處理
if (error != JVMTI_ERROR_NONE) {
throw AgentException(error);
}
}
static jvmtiEnv * m_jvmti;
static char* m_filter;
};
MethodTraceAgent.cpp
#include <iostream>
#include <string.h>
#include <stdio.h>
#include "MethodTraceAgent.h"
#include "<vmti.h>"
using namespace std;
jvmtiEnv* MethodTraceAgent::m_jvmti = 0;
char* MethodTraceAgent::m_filter = 0;
MethodTraceAgent::~MethodTraceAgent()
{
// 必須釋放內(nèi)存,防止內(nèi)存泄露
m_jvmti->Deallocate(reinterpret_cast<unsigned char*>(m_filter));
}
void MethodTraceAgent::Init(JavaVM *vm) {
jvmtiEnv *jvmti = 0;
jint ret = (vm)->GetEnv(reinterpret_cast<void**>(&jvmti), JVMTI_VERSION_1_0);
if (ret != JNI_OK || jvmti == 0) {
throw AgentException(JVMTI_ERROR_INTERNAL);
}
m_jvmti = jvmti;
}
void MethodTraceAgent::ParseOptions(const char* str)
{
if (str == 0)
return;
const size_t len = strlen(str);
if (len == 0)
return;
// 必須做好內(nèi)存復(fù)制工作
jvmtiError error;
error = m_jvmti->Allocate(len + 1,reinterpret_cast<unsigned char**>(&m_filter));
CheckException(error);
strcpy(m_filter, str);
// 可以在這里進行參數(shù)解析的工作
// ...
}
void MethodTraceAgent::AddCapability()
{
// 創(chuàng)建一個新的環(huán)境
jvmtiCapabilities caps;
memset(&caps, 0, sizeof(caps));
caps.can_generate_method_entry_events = 1;
// 設(shè)置當(dāng)前環(huán)境
jvmtiError error = m_jvmti->AddCapabilities(&caps);
CheckException(error);
}
void MethodTraceAgent::RegisterEvent()
{
// 創(chuàng)建一個新的回調(diào)函數(shù)
jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.MethodEntry = &MethodTraceAgent::HandleMethodEntry;
// 設(shè)置回調(diào)函數(shù)
jvmtiError error;
error = m_jvmti->SetEventCallbacks(&callbacks, static_cast<jint>(sizeof(callbacks)));
CheckException(error);
// 開啟事件監(jiān)聽
error = m_jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_METHOD_ENTRY, 0);
CheckException(error);
}
void JNICALL MethodTraceAgent::HandleMethodEntry(jvmtiEnv* jvmti, JNIEnv* jni, jthread thread, jmethodID method)
{
try {
jvmtiError error;
jclass clazz;
char* name;
char* signature;
// 獲得方法對應(yīng)的類
error = m_jvmti->GetMethodDeclaringClass(method, &clazz);
CheckException(error);
// 獲得類的簽名
error = m_jvmti->GetClassSignature(clazz, &signature, 0);
CheckException(error);
// 獲得方法名字
error = m_jvmti->GetMethodName(method, &name, NULL, NULL);
CheckException(error);
// 根據(jù)參數(shù)過濾不必要的方法
if(m_filter != 0){
if (strcmp(m_filter, name) != 0)
return;
}
cout << signature<< " -> " << name << "(..)"<< endl;
// 必須釋放內(nèi)存锣险,避免內(nèi)存泄露
error = m_jvmti->Deallocate(reinterpret_cast<unsigned char*>(name));
CheckException(error);
error = m_jvmti->Deallocate(reinterpret_cast<unsigned char*>(signature));
CheckException(error);
} catch (AgentException& e) {
cout << "Error when enter HandleMethodEntry: " << e.what() << " [" << e.ErrCode() << "]";
}
}
創(chuàng)建一個java程序
public class MethodTraceTest {
public static void main(String[] args){
MethodTraceTest test = new MethodTraceTest();
test.first();
test.second();
}
public void first(){
System.out.println("=> test first()");
}
public void second(){
System.out.println("=> test second()");
}
}
運行時序圖:操作流程
- 編譯java程序生成class文件
javac MethodTraceTest.java
- 編譯Agent 動態(tài)鏈接庫蹄皱,需要將 JDK 提供的一些頭文件包含進來
Windows:
cl /EHsc -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/win32 -LD MethodTraceAgent.cpp Agent.cpp -FeAgent.dll
Linux:
g++ -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/linux MethodTraceAgent.cpp Main.cpp -fPIC -shared -o libagent.so
注意需要有c++環(huán)境,如果是windows環(huán)境使用vs 可使用cl
Windows下vs2019 c++ toolset
注意編譯動態(tài)庫操作系統(tǒng)類型芯肤,64bit下使用工具"x64 Native Tools Command Prompt for VS 2019"(可在安裝vs后windows搜素框搜索)巷折,具體可參考上面官方文檔
執(zhí)行
上面編譯動態(tài)庫后,win下為dll文件崖咨、linux為so文件锻拘,將編譯后的class文件拷貝到動態(tài)文件下,執(zhí)行命令
java -agentlib:Agent=first -cp ./ MethodTraceTest
結(jié)果如下說明成功監(jiān)聽到虛擬機相應(yīng)事件
Agent_OnLoad(000000006990F4A0)
LMethodTraceTest; -> first(..)
=> Call first()
=> Call second()
Agent_OnUnload(000000006990F4A0)
- 至此算是初步了解jvmti击蹲,后續(xù)再詳解另一種方式以及更多jvmti相關(guān)操作