日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

JVM源码分析之javaagent原理完全解读--转

發(fā)布時(shí)間:2025/4/5 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 JVM源码分析之javaagent原理完全解读--转 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

原文地址:http://www.infoq.com/cn/articles/javaagent-illustrated

概述

本文重點(diǎn)講述javaagent的具體實(shí)現(xiàn),因?yàn)樗嫦虻氖俏覀僇ava程序員,而且agent都是用Java編寫的,不需要太多的C/C++編程基礎(chǔ),不過這篇文章里也會(huì)講到JVMTIAgent(C實(shí)現(xiàn)的),因?yàn)閖avaagent的運(yùn)行還是依賴于一個(gè)特殊的JVMTIAgent。

對(duì)于javaagent,或許大家都聽過,甚至使用過,常見的用法大致如下:

java -javaagent:myagent.jar=mode=test Test

我們通過-javaagent來指定我們編寫的agent的jar路徑(./myagent.jar),以及要傳給agent的參數(shù)(mode=test),在啟動(dòng)的時(shí)候這個(gè)agent就可以做一些我們希望的事了。

javaagent的主要功能如下:

  • 可以在加載class文件之前做攔截,對(duì)字節(jié)碼做修改
  • 可以在運(yùn)行期對(duì)已加載類的字節(jié)碼做變更,但是這種情況下會(huì)有很多的限制,后面會(huì)詳細(xì)說
  • 還有其他一些小眾的功能
    • 獲取所有已經(jīng)加載過的類
    • 獲取所有已經(jīng)初始化過的類(執(zhí)行過clinit方法,是上面的一個(gè)子集)
    • 獲取某個(gè)對(duì)象的大小
    • 將某個(gè)jar加入到bootstrap classpath里作為高優(yōu)先級(jí)被bootstrapClassloader加載
    • 將某個(gè)jar加入到classpath里供AppClassloard去加載
    • 設(shè)置某些native方法的前綴,主要在查找native方法的時(shí)候做規(guī)則匹配

想象一下可以讓程序按照我們預(yù)期的邏輯去執(zhí)行,聽起來是不是挺酷的。

JVMTI

JVMTI全稱JVM Tool Interface,是JVM暴露出來的一些供用戶擴(kuò)展的接口集合。JVMTI是基于事件驅(qū)動(dòng)的,JVM每執(zhí)行到一定的邏輯就會(huì)調(diào)用一些事件的回調(diào)接口(如果有的話),這些接口可以供開發(fā)者擴(kuò)展自己的邏輯。

比如最常見的,我們想在某個(gè)類的字節(jié)碼文件讀取之后、類定義之前修改相關(guān)的字節(jié)碼,從而使創(chuàng)建的class對(duì)象是我們修改之后的字節(jié)碼內(nèi)容,那就可以實(shí)現(xiàn)一個(gè)回調(diào)函數(shù)賦給jvmtiEnv(JVMTI的運(yùn)行時(shí),通常一個(gè)JVMTIAgent對(duì)應(yīng)一個(gè)jvmtiEnv,但是也可以對(duì)應(yīng)多個(gè))的回調(diào)方法集合里的ClassFileLoadHook,這樣在接下來的類文件加載過程中都會(huì)調(diào)用到這個(gè)函數(shù)中,大致實(shí)現(xiàn)如下:,

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其實(shí)就是一個(gè)動(dòng)態(tài)庫,利用JVMTI暴露出來的一些接口來干一些我們想做、但是正常情況下又做不到的事情,不過為了和普通的動(dòng)態(tài)庫進(jìn)行區(qū)分,它一般會(huì)實(shí)現(xiàn)如下的一個(gè)或者多個(gè)函數(shù):

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函數(shù),如果agent是在啟動(dòng)時(shí)加載的,也就是在vm參數(shù)里通過-agentlib來指定的,那在啟動(dòng)過程中就會(huì)去執(zhí)行這個(gè)agent里的Agent_OnLoad函數(shù)。
  • Agent_OnAttach函數(shù),如果agent不是在啟動(dòng)時(shí)加載的,而是我們先attach到目標(biāo)進(jìn)程上,然后給對(duì)應(yīng)的目標(biāo)進(jìn)程發(fā)送load命令來加載,則在加載過程中會(huì)調(diào)用Agent_OnAttach函數(shù)。
  • Agent_OnUnload函數(shù),在agent卸載時(shí)調(diào)用,不過貌似基本上很少實(shí)現(xiàn)它。

其實(shí)我們每天都在和JVMTIAgent打交道,只是你可能沒有意識(shí)到而已,比如我們經(jīng)常使用Eclipse等工具調(diào)試Java代碼,其實(shí)就是利用JRE自帶的jdwp agent實(shí)現(xiàn)的,只是Eclipse等工具在沒讓你察覺的情況下將相關(guān)參數(shù)(類似-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:61349)自動(dòng)加到程序啟動(dòng)參數(shù)列表里了,其中agentlib參數(shù)就用來跟要加載的agent的名字,比如這里的jdwp(不過這不是動(dòng)態(tài)庫的名字,JVM會(huì)做一些名稱上的擴(kuò)展,比如在Linux下會(huì)去找libjdwp.so的動(dòng)態(tài)庫進(jìn)行加載,也就是在名字的基礎(chǔ)上加前綴lib,再加后綴.so),接下來會(huì)跟一堆相關(guān)的參數(shù),將這些參數(shù)傳給Agent_OnLoad或者Agent_OnAttach函數(shù)里對(duì)應(yīng)的options

javaagent

說到j(luò)avaagent,必須要講的是一個(gè)叫做instrument的JVMTIAgent(Linux下對(duì)應(yīng)的動(dòng)態(tài)庫是libinstrument.so),因?yàn)閖avaagent功能就是它來實(shí)現(xiàn)的,另外instrument agent還有個(gè)別名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),這個(gè)名字也完全體現(xiàn)了其最本質(zhì)的功能:就是專門為Java語言編寫的插樁服務(wù)提供支持的。

instrument agent

instrument agent實(shí)現(xiàn)了Agent_OnLoadAgent_OnAttach兩方法,也就是說在使用時(shí),agent既可以在啟動(dòng)時(shí)加載,也可以在運(yùn)行時(shí)動(dòng)態(tài)加載。其中啟動(dòng)時(shí)加載還可以通過類似-javaagent:myagent.jar的方式來間接加載instrument agent,運(yùn)行時(shí)動(dòng)態(tài)加載依賴的是JVM的attach機(jī)制(JVM Attach機(jī)制實(shí)現(xiàn)),通過發(fā)送load命令來加載agent。

instrument agent的核心數(shù)據(jù)結(jié)構(gòu)如下:

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 */ };

這里解釋一下幾個(gè)重要項(xiàng):

  • mNormalEnvironment:主要提供正常的類transform及redefine功能。
  • mRetransformEnvironment:主要提供類retransform功能。
  • mInstrumentationImpl:這個(gè)對(duì)象非常重要,也是我們Java agent和JVM進(jìn)行交互的入口,或許寫過javaagent的人在寫`premain`以及`agentmain`方法的時(shí)候注意到了有個(gè)Instrumentation參數(shù),該參數(shù)其實(shí)就是這里的對(duì)象。
  • mPremainCaller:指向`sun.instrument.InstrumentationImpl.loadClassAndCallPremain`方法,如果agent是在啟動(dòng)時(shí)加載的,則該方法會(huì)被調(diào)用。
  • mAgentmainCaller:指向`sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain`方法,該方法在通過attach的方式動(dòng)態(tài)加載agent的時(shí)候調(diào)用。
  • mTransform:指向`sun.instrument.InstrumentationImpl.transform`方法。
  • mAgentClassName:在我們javaagent的MANIFEST.MF里指定的`Agent-Class`。
  • mOptionsString:傳給agent的一些參數(shù)。
  • mRedefineAvailable:是否開啟了redefine功能,在javaagent的MANIFEST.MF里設(shè)置`Can-Redefine-Classes:true`。
  • mNativeMethodPrefixAvailable:是否支持native方法前綴設(shè)置,同樣在javaagent的MANIFEST.MF里設(shè)置`Can-Set-Native-Method-Prefix:true`。
  • mIsRetransformer:如果在javaagent的MANIFEST.MF文件里定義了`Can-Retransform-Classes:true`,將會(huì)設(shè)置mRetransformEnvironment的mIsRetransformer為true。

在啟動(dòng)時(shí)加載instrument agent

正如前面“概述”里提到的方式,就是啟動(dòng)時(shí)加載instrument agent,具體過程都在`InvocationAdapter.c`的`Agent_OnLoad`方法里,這里簡(jiǎn)單描述下過程:

  • 創(chuàng)建并初始化JPLISAgent
  • 監(jiān)聽VMInit事件,在vm初始化完成之后做下面的事情:
    • 創(chuàng)建InstrumentationImpl對(duì)象
    • 監(jiān)聽ClassFileLoadHook事件
    • 調(diào)用InstrumentationImpl的`loadClassAndCallPremain`方法,在這個(gè)方法里會(huì)調(diào)用javaagent里MANIFEST.MF里指定的`Premain-Class`類的premain方法
  • 解析javaagent里MANIFEST.MF里的參數(shù),并根據(jù)這些參數(shù)來設(shè)置JPLISAgent里的一些內(nèi)容

在運(yùn)行時(shí)加載instrument agent

在運(yùn)行時(shí)加載的方式,大致按照下面的方式來操作:

VirtualMachine vm = VirtualMachine.attach(pid); vm.loadAgent(agentPath, agentArgs);

上面會(huì)通過JVM的attach機(jī)制來請(qǐng)求目標(biāo)JVM加載對(duì)應(yīng)的agent,過程大致如下:

  • 創(chuàng)建并初始化JPLISAgent
  • 解析javaagent里MANIFEST.MF里的參數(shù)
  • 創(chuàng)建InstrumentationImpl對(duì)象
  • 監(jiān)聽ClassFileLoadHook事件
  • 調(diào)用InstrumentationImpl的loadClassAndCallAgentmain方法,在這個(gè)方法里會(huì)調(diào)用javaagent里MANIFEST.MF里指定的Agent-Class類的agentmain方法

instrument agent的ClassFileLoadHook回調(diào)實(shí)現(xiàn)

不管是啟動(dòng)時(shí)還是運(yùn)行時(shí)加載的instrument agent,都關(guān)注著同一個(gè)jvmti事件——ClassFileLoadHook,這個(gè)事件是在讀取字節(jié)碼文件之后回調(diào)時(shí)用的,這樣可以對(duì)原來的字節(jié)碼做修改,那這里面究竟是怎樣實(shí)現(xiàn)的呢?

void JNICALLeventHandlerClassFileLoadHook( 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); } }

先根據(jù)jvmtiEnv取得對(duì)應(yīng)的JPLISEnvironment,因?yàn)樯厦嫖乙呀?jīng)說到其實(shí)有兩個(gè)JPLISEnvironment(并且有兩個(gè)jvmtiEnv),其中一個(gè)是專門做retransform的,而另外一個(gè)用來做其他事情,根據(jù)不同的用途,在注冊(cè)具體的ClassFileTransformer時(shí)也是分開的,對(duì)于作為retransform用的ClassFileTransformer,我們會(huì)注冊(cè)到一個(gè)單獨(dú)的TransformerManager里。

接著調(diào)用transformClassFile方法,由于函數(shù)實(shí)現(xiàn)比較長(zhǎng),這里就不貼代碼了,大致意思就是調(diào)用InstrumentationImpl對(duì)象的transform方法,根據(jù)最后那個(gè)參數(shù)來決定選哪個(gè)TransformerManager里的ClassFileTransformer對(duì)象們做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; }

以上是最終調(diào)到的java代碼,可以看到已經(jīng)調(diào)用到我們自己編寫的javaagent代碼里了,我們一般是實(shí)現(xiàn)一個(gè)ClassFileTransformer類,然后創(chuàng)建一個(gè)對(duì)象注冊(cè)到對(duì)應(yīng)的TransformerManager里。

Class Transform的實(shí)現(xiàn)

這里說的class transform其實(shí)是狹義的,主要是針對(duì)第一次類文件加載時(shí)就要求被transform的場(chǎng)景,在加載類文件的時(shí)候發(fā)出ClassFileLoad事件,然后交給instrumenat agent來調(diào)用javaagent里注冊(cè)的ClassFileTransformer實(shí)現(xiàn)字節(jié)碼的修改。

Class Redefine的實(shí)現(xiàn)

類重新定義,這是Instrumentation提供的基礎(chǔ)功能之一,主要用在已經(jīng)被加載過的類上,想對(duì)其進(jìn)行修改,要做這件事,我們必須要知道兩個(gè)東西,一個(gè)是要修改哪個(gè)類,另外一個(gè)是想將那個(gè)類修改成怎樣的結(jié)構(gòu),有了這兩個(gè)信息之后就可以通過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里對(duì)應(yīng)的實(shí)現(xiàn)是創(chuàng)建一個(gè)VM_RedefineClassesVM_Operation,注意執(zhí)行它的時(shí)候會(huì)stop-the-world:

jvmtiErrorJvmtiEnv::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 */

這個(gè)過程我盡量用語言來描述清楚,不詳細(xì)貼代碼了,因?yàn)榇a量實(shí)在有點(diǎn)大:

  • 挨個(gè)遍歷要批量重定義的jvmtiClassDefinition
  • 然后讀取新的字節(jié)碼,如果有關(guān)注ClassFileLoadHook事件的,還會(huì)走對(duì)應(yīng)的transform來對(duì)新的字節(jié)碼再做修改
  • 字節(jié)碼解析好,創(chuàng)建一個(gè)klassOop對(duì)象
  • 對(duì)比新老類,并要求如下:
    • 父類是同一個(gè)
    • 實(shí)現(xiàn)的接口數(shù)也要相同,并且是相同的接口
    • 類訪問符必須一致
    • 字段數(shù)和字段名要一致
    • 新增的方法必須是private static/final的
    • 可以刪除修改方法
  • 對(duì)新類做字節(jié)碼校驗(yàn)
  • 合并新老類的常量池
  • 如果老類上有斷點(diǎn),那都清除掉
  • 對(duì)老類做JIT去優(yōu)化
  • 對(duì)新老方法匹配的方法的jmethodId做更新,將老的jmethodId更新到新的method上
  • 新類的常量池的holer指向老的類
  • 將新類和老類的一些屬性做交換,比如常量池,methods,內(nèi)部類
  • 初始化新的vtable和itable
  • 交換annotation的method、field、paramenter
  • 遍歷所有當(dāng)前類的子類,修改他們的vtable及itable

上面是基本的過程,總的來說就是只更新了類里的內(nèi)容,相當(dāng)于只更新了指針指向的內(nèi)容,并沒有更新指針,避免了遍歷大量已有類對(duì)象對(duì)它們進(jìn)行更新所帶來的開銷。

Class Retransform的實(shí)現(xiàn)

retransform class可以簡(jiǎn)單理解為回滾操作,具體回滾到哪個(gè)版本,這個(gè)需要看情況而定,下面不管那種情況都有一個(gè)前提,那就是javaagent已經(jīng)要求要有retransform的能力了:

  • 如果類是在第一次加載的的時(shí)候就做了transform,那么做retransform的時(shí)候會(huì)將代碼回滾到transform之后的代碼
  • 如果類是在第一次加載的的時(shí)候沒有任何變化,那么做retransform的時(shí)候會(huì)將代碼回滾到最原始的類文件里的字節(jié)碼
  • 如果類已經(jīng)加載了,期間類可能做過多次redefine(比如被另外一個(gè)agent做過),但是接下來加載一個(gè)新的agent要求有retransform的能力了,然后對(duì)類做redefine的動(dòng)作,那么retransform的時(shí)候會(huì)將代碼回滾到上一個(gè)agent最后一次做redefine后的字節(jié)碼

我們從InstrumentationImpl的retransformClasses方法參數(shù)看猜到應(yīng)該是做回滾操作,因?yàn)槲覀冎恢付薱lass:

public void retransformClasses(Class<?>[] classes) { if (!isRetransformClassesSupported()) { throw new UnsupportedOperationException( "retransformClasses is not supported in this environment"); } retransformClasses0(mNativeAgent, classes); }

不過retransform的實(shí)現(xiàn)其實(shí)也是通過redefine的功能來實(shí)現(xiàn),在類加載的時(shí)候有比較小的差別,主要體現(xiàn)在究竟會(huì)走哪些transform上,如果當(dāng)前是做retransform的話,那將忽略那些注冊(cè)到正常的TransformerManager里的ClassFileTransformer,而只會(huì)走專門為retransform而準(zhǔn)備的TransformerManager的ClassFileTransformer,不然想象一下字節(jié)碼又被無聲無息改成某個(gè)中間態(tài)了。

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除了做字節(jié)碼上面的修改之外,其實(shí)還有一些小功能,有時(shí)候還是挺有用的

  • 獲取所有已經(jīng)被加載的類:Class[] getAllLoadedClasses();?
  • 獲取所有已經(jīng)初始化了的類:?Class[] getInitiatedClasses(ClassLoader loader);?
  • 獲取某個(gè)對(duì)象的大小:?long getObjectSize(Object objectToSize);?
  • 將某個(gè)jar加入到bootstrap classpath里優(yōu)先其他jar被加載:?void appendToBootstrapClassLoaderSearch(JarFile jarfile);?
  • 將某個(gè)jar加入到classpath里供appclassloard去加載:void appendToSystemClassLoaderSearch(JarFile jarfile);?
  • 設(shè)置某些native方法的前綴,主要在找native方法的時(shí)候做規(guī)則匹配:?void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)。

轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/7550707.html

《新程序員》:云原生和全面數(shù)字化實(shí)踐50位技術(shù)專家共同創(chuàng)作,文字、視頻、音頻交互閱讀

總結(jié)

以上是生活随笔為你收集整理的JVM源码分析之javaagent原理完全解读--转的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。