深入剖析理解AsyncGetCallTrace源碼底層原理
前言
AsyncGetCallTrace 是由 OracleJDK/OpenJDK 內(nèi)部提供的一個(gè)函數(shù),該函數(shù)可以在 JVM 未進(jìn)入 safepoint 時(shí)正常獲取到當(dāng)前線程的調(diào)用棧(換句話說(shuō),使用該函數(shù)獲取線程棧時(shí),不會(huì)要求 JVM 進(jìn)入 safepoint。而進(jìn)入 safepoint 對(duì)于 OpenJDK或者 OracleJDK 來(lái)說(shuō)意味著會(huì) STW 的發(fā)生,所以這意味著使用該函數(shù)獲取線程棧不會(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)前線程的調(diào)用棧即可。由于 UNIX 信號(hào)會(huì)被隨機(jī)的發(fā)送給進(jìn)程的某一線程進(jìn)行處理,因此可以認(rè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è)特定線程的,這個(gè)線程是由trace->env_id 唯一標(biāo)識(shí)的,而且 該標(biāo)識(shí)的線程 必須和當(dāng)前執(zhí)行 AsyncGetCallTrace 方法的 線程 是同一線程。同時(shí)調(diào)用者需要為 trace->frames 分配足夠多的內(nèi)存,來(lái)保存棧深最多為 depth 的棧。若獲取到了有關(guān)的棧,JVM 會(huì)自動(dòng)把相關(guān)的堆棧信息寫入 trace 中。接下來(lái)我們通過(guò)源碼來(lái)看看內(nèi)部實(shí)現(xiàn)。
AsyncGetCallTrace 實(shí)現(xiàn)
具體分析直接看代碼里面的注釋,為了保持完整性,該方法的任何代碼我都沒(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)的線程是否有效,或者該線程是否已經(jīng)退出,
// 任一條件滿足,則返回 ticks_thread_exit(對(duì)應(yīng)為核心數(shù)據(jù)結(jié)構(gòu)中枚舉類型所示)
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 所指線程是否是當(dāng)前線程進(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ù)線程的當(dāng)前狀態(tài)來(lái)獲取對(duì)應(yīng)的線程棧,只有在線程的狀態(tài)為 _thread_in_vm/_thread_in_vm_trans
// 和 _thread_in_Java/_thread_in_Java_trans 時(shí)才會(huì)進(jì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)前線程的棧頂棧幀
// 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 {
// 該線程如果沒(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;
}
}
從以上的分析中,最終獲取線程棧的地方主要在這兩個(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)前線程的棧頂幀。后面跟了個(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)前線程,至于 isInJava 入?yún)⑹呛彤?dāng)前的線程的狀態(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)前線程只能是 java 線程
- 判斷棧頂幀是否存在,并且當(dāng)前的棧是 walkable 的,若二者的滿足,則返回 javaThread 的 pd_last_frame,即棧頂幀,結(jié)束;否則繼續(xù);
- 如果當(dāng)前線程是跑 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ī)的情況下可以獲取線程棧的函數(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è)線程的棧,為高性能的熱點(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 接口的線程安全版本。但是我們看到實(shí)際上,這個(gè)方法中所有代碼都是未加鎖的,為啥?細(xì)心的同學(xué)可能已經(jīng)發(fā)現(xiàn),因?yàn)樵摵瘮?shù)的調(diào)用是用信號(hào)處理函數(shù)調(diào)用,且只有某一個(gè)單獨(dú)線程同時(shí)運(yùn)行它,所以它的使用場(chǎng)景就天然決定了它是線程安全的。
- 還有最后一點(diǎn)要注意,由于該方法的調(diào)用是在 java 線程中調(diào)用的,所以當(dāng)使用者發(fā)送
SIGPROF信號(hào)時(shí),恰好由于進(jìn)程處于 GC 階段,而導(dǎo)致 java 線程處于安全點(diǎn)而被阻塞,從而導(dǎo)致此時(shí)無(wú)法執(zhí)行該方法而獲取線程棧的場(chǎng)景,或者在執(zhí)行過(guò)程中線程被有關(guān)安全點(diǎn)掛起而導(dǎo)致獲取線程棧失敗這兩個(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)文章
Spring的refresh()方法相關(guān)異常解析
這篇文章主要介紹了Spring的refresh()方法相關(guān)異常解析,具有一定參考價(jià)值,需要的朋友可以了解下。2017-11-11
java自定義切面增強(qiáng)方式(關(guān)于自定義注解aop)
這篇文章主要介紹了java自定義切面增強(qiáng)方式(關(guān)于自定義注解aop),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-04-04
JAVA中調(diào)用C語(yǔ)言函數(shù)的實(shí)現(xiàn)方式
這篇文章主要介紹了JAVA中調(diào)用C語(yǔ)言函數(shù)的實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08
java實(shí)現(xiàn)幸運(yùn)抽獎(jiǎng)系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)幸運(yùn)抽獎(jiǎng)系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-07-07
Java中的對(duì)象和對(duì)象引用實(shí)例淺析
這篇文章主要介紹了Java中的對(duì)象和對(duì)象引用,實(shí)例分析了對(duì)象與對(duì)象引用的概念與相關(guān)使用技巧,需要的朋友可以參考下2015-05-05
Java進(jìn)程cpu頻繁100%問(wèn)題解決方案
這篇文章主要介紹了Java進(jìn)程cpu頻繁100%問(wèn)題解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10

