深入剖析理解AsyncGetCallTrace源碼底層原理
前言
AsyncGetCallTrace 是由 OracleJDK/OpenJDK 內(nèi)部提供的一個(gè)函數(shù),該函數(shù)可以在 JVM 未進(jìn)入 safepoint 時(shí)正常獲取到當(dāng)前線(xiàn)程的調(diào)用棧(換句話(huà)說(shuō),使用該函數(shù)獲取線(xiàn)程棧時(shí),不會(huì)要求 JVM 進(jìn)入 safepoint。而進(jìn)入 safepoint 對(duì)于 OpenJDK或者 OracleJDK 來(lái)說(shuō)意味著會(huì) STW 的發(fā)生,所以這意味著使用該函數(shù)獲取線(xiàn)程棧不會(huì)產(chǎn)生 STW,It’s amazing.)。目前該函數(shù)僅在 Linux X86、Solaris SPARC、Solaris X86 系統(tǒng)下支持。
另外它還支持在 UNIX 信號(hào)處理器中被異步調(diào)用,那么我們只需注冊(cè)一個(gè) UNIX 信號(hào)處理器,并在Handler中調(diào)用 AsyncGetCallTrace 獲取當(dāng)前線(xiàn)程的調(diào)用棧即可。由于 UNIX 信號(hào)會(huì)被隨機(jī)的發(fā)送給進(jìn)程的某一線(xiàn)程進(jìn)行處理,因此可以認(rèn)為獲取所有線(xiàn)程的調(diào)用棧樣本是均勻的。
但是值得注意的是,該函數(shù)不是標(biāo)準(zhǔn)的 JVM API,所以使用的時(shí)候,可能存在以下問(wèn)題:
移植性問(wèn)題,因?yàn)橹荒芘茉?OpenJDK 或者 OracleJDK 上
由于不是標(biāo)準(zhǔn)的 JVMTI API,所以使用者需要通過(guò)特殊一些方式來(lái)獲取該函數(shù),這給使用者帶來(lái)了一些不便,但是這也無(wú)大礙。
源碼實(shí)現(xiàn)
關(guān)于怎么使用該函數(shù)去進(jìn)行熱點(diǎn)方法采樣的方法,不在本節(jié)的討論范圍,在參考資料中,有一些描述,如果還有不清楚的,也可以給我留言交流。
核心數(shù)據(jù)結(jié)構(gòu)
// call frame copied from old .h file and renamed // Fields: // 1) For Java frame (interpreted and compiled), // lineno - bci of the method being executed or -1 if bci is not available // method_id - jmethodID of the method being executed // 2) For native method // lineno - (-3) // method_id - jmethodID of the method being executed typedef struct { jint lineno; // numberline number in the source file jmethodID method_id; // method executed in this frame } ASGCT_CallFrame; // call trace copied from old .h file and renamed // Fields: // env_id - ID of thread which executed this trace. // num_frames - number of frames in the trace. // (< 0 indicates the frame is not walkable). // frames - the ASGCT_CallFrames that make up this trace. Callee followed by callers. typedef struct { JNIEnv *env_id; // Env where trace was recorded jint num_frames; // number of frames in this trace ASGCT_CallFrame *frames; // frames } ASGCT_CallTrace; // These name match the names reported by the forte quality kit // 這些枚舉是對(duì)應(yīng)到 ASGCT_CallTrace 中的 num_frames 的返回值的 // 舉個(gè)例子,當(dāng) JVM 在進(jìn)行 GC 時(shí),返回值中 // ASGCT_CallTrace.num_frames == ticks_GC_active enum { ticks_no_Java_frame = 0, ticks_no_class_load = -1, ticks_GC_active = -2, ticks_unknown_not_Java = -3, ticks_not_walkable_not_Java = -4, ticks_unknown_Java = -5, ticks_not_walkable_Java = -6, ticks_unknown_state = -7, ticks_thread_exit = -8, ticks_deopt = -9, ticks_safepoint = -10 };
函數(shù)申明
// trace - trace data structure to be filled by the VM. // depth - depth of the call stack trace. // ucontext - ucontext_t of the LWP void AsyncGetCallTrace(ASGCT_CallTrace *trace, jint depth, void* ucontext)
該函數(shù)的調(diào)用者獲取到的棧是屬于某一個(gè)特定線(xiàn)程的,這個(gè)線(xiàn)程是由trace->env_id
唯一標(biāo)識(shí)的,而且 該標(biāo)識(shí)的線(xiàn)程 必須和當(dāng)前執(zhí)行 AsyncGetCallTrace 方法的 線(xiàn)程 是同一線(xiàn)程。同時(shí)調(diào)用者需要為 trace->frames 分配足夠多的內(nèi)存,來(lái)保存棧深最多為 depth 的棧。若獲取到了有關(guān)的棧,JVM 會(huì)自動(dòng)把相關(guān)的堆棧信息寫(xiě)入 trace 中。接下來(lái)我們通過(guò)源碼來(lái)看看內(nèi)部實(shí)現(xiàn)。
AsyncGetCallTrace 實(shí)現(xiàn)
具體分析直接看代碼里面的注釋?zhuān)瑸榱吮3滞暾?,該方法的任何代碼我都沒(méi)刪除,源碼位置:/hotspot/src/share/vm/prims/forte.cpp
void AsyncGetCallTrace(ASGCT_CallTrace *trace, jint depth, void* ucontext) { JavaThread* thread; // 1. 首先判斷 jniEnv 是否為空,或者 jniEnv 對(duì)應(yīng)的線(xiàn)程是否有效,或者該線(xiàn)程是否已經(jīng)退出, // 任一條件滿(mǎn)足,則返回 ticks_thread_exit(對(duì)應(yīng)為核心數(shù)據(jù)結(jié)構(gòu)中枚舉類(lèi)型所示) if (trace->env_id == NULL || (thread = JavaThread::thread_from_jni_environment(trace->env_id)) == NULL || thread->is_exiting()) { // bad env_id, thread has exited or thread is exiting trace->num_frames = ticks_thread_exit; // -8 return; } if (thread->in_deopt_handler()) { // thread is in the deoptimization handler so return no frames trace->num_frames = ticks_deopt; // -9 return; } // 2. 這里對(duì) jniEnv 所指線(xiàn)程是否是當(dāng)前線(xiàn)程進(jìn)行斷言,如果不相等則直接報(bào)錯(cuò) assert(JavaThread::current() == thread, "AsyncGetCallTrace must be called by the current interrupted thread"); // 3. JVMTI_EVENT_CLASS_LOAD 事件必須 enable,否則直接返回 ticks_no_class_load if (!JvmtiExport::should_post_class_load()) { trace->num_frames = ticks_no_class_load; // -1 return; } // 4. 當(dāng)前 heap 必須沒(méi)有進(jìn)行 GC ,否則直接返回 ticks_GC_active if (Universe::heap()->is_gc_active()) { trace->num_frames = ticks_GC_active; // -2 return; } // 5. 根據(jù)線(xiàn)程的當(dāng)前狀態(tài)來(lái)獲取對(duì)應(yīng)的線(xiàn)程棧,只有在線(xiàn)程的狀態(tài)為 _thread_in_vm/_thread_in_vm_trans // 和 _thread_in_Java/_thread_in_Java_trans 時(shí)才會(huì)進(jìn)行線(xiàn)程棧的爬取 switch (thread->thread_state()) { case _thread_new: case _thread_uninitialized: case _thread_new_trans: // We found the thread on the threads list above, but it is too // young to be useful so return that there are no Java frames. trace->num_frames = 0; break; case _thread_in_native: case _thread_in_native_trans: case _thread_blocked: case _thread_blocked_trans: case _thread_in_vm: case _thread_in_vm_trans: { frame fr; // 首先獲取當(dāng)前線(xiàn)程的棧頂棧幀 // param isInJava == false - indicate we aren't in Java code if (!thread->pd_get_top_frame_for_signal_handler(&fr, ucontext, false)) { trace->num_frames = ticks_unknown_not_Java; // -3 unknown frame } else { // 該線(xiàn)程如果沒(méi)有任何的 Java 棧幀,直接返回 0 幀 if (!thread->has_last_Java_frame()) { trace->num_frames = 0; // No Java frames } else { trace->num_frames = ticks_not_walkable_not_Java; // -4 non walkable frame by default // 如果存在合法的棧幀,則填充 trace 中的 frames 和 num_frames forte_fill_call_trace_given_top(thread, trace, depth, fr); ... } } } break; case _thread_in_Java: case _thread_in_Java_trans: { frame fr; // param isInJava == true - indicate we are in Java code if (!thread->pd_get_top_frame_for_signal_handler(&fr, ucontext, true)) { trace->num_frames = ticks_unknown_Java; // -5 unknown frame } else { trace->num_frames = ticks_not_walkable_Java; // -6, non walkable frame by default forte_fill_call_trace_given_top(thread, trace, depth, fr); } } break; default: // Unknown thread state trace->num_frames = ticks_unknown_state; // -7 break; } }
從以上的分析中,最終獲取線(xiàn)程棧的地方主要在這兩個(gè)方法 pd_get_top_frame_for_signal_handler
和 forte_fill_call_trace_given_top
,接下來(lái)我們來(lái)看下這兩個(gè)方法的實(shí)現(xiàn)。
pd_get_top_frame_for_signal_handler 實(shí)現(xiàn)
從方法名不難看出,該方法的主要作用是獲取當(dāng)前線(xiàn)程的棧頂幀。后面跟了個(gè) signal_handler,最初的想法我猜應(yīng)該是為響應(yīng) UNIX 下的 SIGPROF
信號(hào)的。因?yàn)楸旧?nbsp;AsyncGetCallTrace
就是為此而生的。該方法的源碼位置 /hotspot/src/os_cpu/linux_x86/vm/thread_linux_x86.cpp
bool JavaThread::pd_get_top_frame_for_signal_handler(frame* fr_addr, void* ucontext, bool isInJava) { assert(Thread::current() == this, "caller must be current thread"); return pd_get_top_frame(fr_addr, ucontext, isInJava); }
很簡(jiǎn)單,判斷一下是否是當(dāng)前線(xiàn)程,至于 isInJava 入?yún)⑹呛彤?dāng)前的線(xiàn)程的狀態(tài)相關(guān)的,如果是跑在 java 代碼內(nèi),則為 true,否則為 false。
pd_get_top_frame 實(shí)現(xiàn)
bool JavaThread::pd_get_top_frame(frame* fr_addr, void* ucontext, bool isInJava) { assert(this->is_Java_thread(), "must be JavaThread"); JavaThread* jt = (JavaThread *)this; // If we have a last_Java_frame, then we should use it even if // isInJava == true. It should be more reliable than ucontext info. if (jt->has_last_Java_frame() && jt->frame_anchor()->walkable()) { *fr_addr = jt->pd_last_frame(); return true; } // At this point, we don't have a last_Java_frame, so // we try to glean some information out of the ucontext // if we were running Java code when SIGPROF came in. if (isInJava) { ucontext_t* uc = (ucontext_t*) ucontext; intptr_t* ret_fp; intptr_t* ret_sp; ExtendedPC addr = os::Linux::fetch_frame_from_ucontext(this, uc, &ret_sp, &ret_fp); if (addr.pc() == NULL || ret_sp == NULL ) { // ucontext wasn't useful return false; } frame ret_frame(ret_sp, ret_fp, addr.pc()); if (!ret_frame.safe_for_sender(jt)) { #ifdef COMPILER2 // C2 uses ebp as a general register see if NULL fp helps frame ret_frame2(ret_sp, NULL, addr.pc()); if (!ret_frame2.safe_for_sender(jt)) { // nothing else to try if the frame isn't good return false; } ret_frame = ret_frame2; #else // nothing else to try if the frame isn't good return false; #endif /* COMPILER2 */ } *fr_addr = ret_frame; return true; } // nothing else to try return false; }
實(shí)際上拿棧頂幀的函數(shù),由于函數(shù)的源碼較長(zhǎng),我就簡(jiǎn)短的描述一下邏輯
- 當(dāng)前線(xiàn)程只能是 java 線(xiàn)程
- 判斷棧頂幀是否存在,并且當(dāng)前的棧是 walkable 的,若二者的滿(mǎn)足,則返回 javaThread 的 pd_last_frame,即棧頂幀,結(jié)束;否則繼續(xù);
- 如果當(dāng)前線(xiàn)程是跑 java 代碼,那么我們嘗試在 ucontext_t 內(nèi)收集一些我們需要的信息,比如說(shuō)棧幀
forte_fill_call_trace_given_top 實(shí)現(xiàn)
當(dāng)我們獲取到棧頂?shù)膸?,接下?lái)的事情就順理成章了,只要從棧頂開(kāi)始,遍歷整個(gè)堆棧就能把所有的方法都獲取到了,同時(shí)將獲取到的結(jié)果保存到ASGCT_CallTrace
,源碼位置:/hotspot/src/share/vm/prims/forte.cpp
static void forte_fill_call_trace_given_top(JavaThread* thd, ASGCT_CallTrace* trace, int depth, frame top_frame) { NoHandleMark nhm; frame initial_Java_frame; Method* method; int bci = -1; // assume BCI is not available for method // update with correct information if available int count; count = 0; assert(trace->frames != NULL, "trace->frames must be non-NULL"); // 1. 獲取到棧頂?shù)牡谝粋€(gè) java 棧幀 // Walk the stack starting from 'top_frame' and search for an initial Java frame. find_initial_Java_frame(thd, &top_frame, &initial_Java_frame, &method, &bci); // Check if a Java Method has been found. if (method == NULL) return; // 2. 如果不是合法的方法,直接返回 if (!method->is_valid_method()) { trace->num_frames = ticks_GC_active; // -2 return; } vframeStreamForte st(thd, initial_Java_frame, false); // 循環(huán)迭代棧上的所有棧幀,一一獲取每個(gè)方法 bci 和 方法 id,這里會(huì)用上從外面?zhèn)魅氲淖畲髼I?depth for (; !st.at_end() && count < depth; st.forte_next(), count++) { bci = st.bci(); method = st.method(); if (!method->is_valid_method()) { // we throw away everything we've gathered in this sample since // none of it is safe trace->num_frames = ticks_GC_active; // -2 return; } // 根據(jù)方法對(duì)象獲取方法 id,如果方法 id 在此時(shí)還未產(chǎn)生,則返回 NULL trace->frames[count].method_id = method->find_jmethod_id_or_null(); // 如果方法是不是 native 方法,則把 lineno 設(shè)置為 bci 的值,否則置 -3 if (!method->is_native()) { trace->frames[count].lineno = bci; } else { trace->frames[count].lineno = -3; } } trace->num_frames = count; return; }
總結(jié)
源碼貼的有點(diǎn)多,這里稍微做一個(gè)小的總結(jié),同時(shí)也說(shuō)明一下使用時(shí)的一些注意事項(xiàng)
- AsyncGetCallTrace 是 OpenJDK/OracleJDK 提供的可以在不暫停虛擬機(jī)的情況下可以獲取線(xiàn)程棧的函數(shù),開(kāi)發(fā)人員的主要觸發(fā)點(diǎn)是通過(guò) UNIX 的
SIGPROF
信號(hào)來(lái)觸發(fā)信號(hào)的 handler 來(lái)調(diào)用此函數(shù),來(lái)隨機(jī)獲取某一個(gè)線(xiàn)程的棧,為高性能的熱點(diǎn)方法監(jiān)控提供了可行的技術(shù)支持; - AsyncGetCallTrace 的使用必須在 Agent onload 的時(shí)候 Enable JVMTI_EVENT_CLASS_LOAD 和 JVMTI_EVENT_CLASS_PREPARE 事件,不然無(wú)法獲取相關(guān)的方法,同時(shí)還需要注冊(cè) callbacks.ClassPrepare 事件,在 class 加載準(zhǔn)備階段預(yù)先生成好 jMethodId,不然可能出現(xiàn) jMethodId 為空的情況;
- 實(shí)際上,AsyncGetCallTrace 還可以認(rèn)為是標(biāo)準(zhǔn) JVM-TI 中的 GetCallTrace 接口的線(xiàn)程安全版本。但是我們看到實(shí)際上,這個(gè)方法中所有代碼都是未加鎖的,為啥?細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn),因?yàn)樵摵瘮?shù)的調(diào)用是用信號(hào)處理函數(shù)調(diào)用,且只有某一個(gè)單獨(dú)線(xiàn)程同時(shí)運(yùn)行它,所以它的使用場(chǎng)景就天然決定了它是線(xiàn)程安全的。
- 還有最后一點(diǎn)要注意,由于該方法的調(diào)用是在 java 線(xiàn)程中調(diào)用的,所以當(dāng)使用者發(fā)送
SIGPROF
信號(hào)時(shí),恰好由于進(jìn)程處于 GC 階段,而導(dǎo)致 java 線(xiàn)程處于安全點(diǎn)而被阻塞,從而導(dǎo)致此時(shí)無(wú)法執(zhí)行該方法而獲取線(xiàn)程棧的場(chǎng)景,或者在執(zhí)行過(guò)程中線(xiàn)程被有關(guān)安全點(diǎn)掛起而導(dǎo)致獲取線(xiàn)程棧失敗這兩個(gè)場(chǎng)景發(fā)生。
本文中難免存在一些錯(cuò)誤和不足,還望大家不吝指出,同時(shí)如果對(duì)文中描述存在任何疑問(wèn),也歡迎大家提出來(lái)討論。
以上就是深入剖析理解AsyncGetCallTrace源碼的詳細(xì)內(nèi)容,更多關(guān)于AsyncGetCallTrace源碼剖析的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Assert.assertEquals的使用方法及注意事項(xiàng)說(shuō)明
這篇文章主要介紹了Assert.assertEquals的使用方法及注意事項(xiàng)說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05spring通過(guò)filter,Interceptor統(tǒng)一處理ResponseBody的返回值操作
這篇文章主要介紹了spring通過(guò)filter,Interceptor統(tǒng)一處理ResponseBody的返回值操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09@Configuration保證@Bean單例語(yǔ)義方法介紹
這篇文章主要介紹了SpringBoot中的@Configuration與@Bean注解,在進(jìn)行項(xiàng)目編寫(xiě)前,我們還需要知道一個(gè)東西,就是SpringBoot對(duì)我們的SpringMVC還做了哪些配置,包括如何擴(kuò)展,如何定制,只有把這些都搞清楚了,我們?cè)谥笫褂貌艜?huì)更加得心應(yīng)手2023-01-01springboot使用小工具之Lombok、devtools、Spring Initailizr詳解
這篇文章主要介紹了springboot使用小工具之Lombok、devtools、Spring Initailizr詳解,Lombok可以代替手寫(xiě)get、set、構(gòu)造方法等,需要idea裝插件lombok,本文通過(guò)示例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10java基于swing實(shí)現(xiàn)的五子棋游戲代碼
這篇文章主要介紹了java基于swing實(shí)現(xiàn)的五子棋游戲代碼,主要涉及圖形界面與數(shù)組的用法,有不錯(cuò)的參考借鑒價(jià)值,需要的朋友可以參考下2014-11-11SpringBoot如何使用Undertow做服務(wù)器
這篇文章主要介紹了SpringBoot如何使用Undertow做服務(wù)器,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07java動(dòng)態(tài)添加外部jar包到classpath的實(shí)例詳解
這篇文章主要介紹了java動(dòng)態(tài)添加外部jar包到classpath的實(shí)例詳解的相關(guān)資料,希望通過(guò)本文能幫助到大家,需要的朋友可以參考下2017-09-09