JVM方法執(zhí)行的來(lái)龍去脈

趁著春節(jié)放假,借著《揭秘Java虛擬機(jī)》秽之,好好看了下Hotspot源碼当娱,對(duì)JVM執(zhí)行Java方法的過(guò)程有了更深入的了解。大過(guò)年的考榨,不發(fā)紅包跨细,發(fā)篇文章吧。

一:CallStub例程

普通的Java類被編譯成字節(jié)碼后河质,對(duì)Java方法的調(diào)用都會(huì)轉(zhuǎn)換為invoke指令冀惭,而Java第一個(gè)方法是由誰(shuí)調(diào)用的呢?Java main()方法的執(zhí)行其實(shí)是通過(guò)JVM自己調(diào)用的掀鹅。不過(guò)對(duì)于JVM來(lái)說(shuō)散休,無(wú)論是如何執(zhí)行Java方法,都是通過(guò)JavaCalls模塊來(lái)實(shí)現(xiàn)的乐尊。

JavaCalls這個(gè)名字取得很形象戚丸,一看就知道是用來(lái)調(diào)用Java方法的。JavaCalls中有很多用來(lái)調(diào)用Java方法的函數(shù)科吭,如call_virtual()昏滴、call_special()、call_static等对人,用來(lái)調(diào)用不同類型的Java方法谣殊,不過(guò)這些函數(shù)最終都是調(diào)用的call()方法:

void JavaCalls::call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS) {
  ......
  os::os_exception_wrapper(call_helper, result, &method, args, THREAD);
}

os::os_exception_wrapper(call_helper, result, &method, args, THREAD)中其實(shí)沒(méi)啥:

void os::os_exception_wrapper(java_call_t f, JavaValue* value, methodHandle* method,
                     JavaCallArguments* args, Thread* thread) {
  f(value, method, args, thread);
}

f其實(shí)就是call()方法中傳入的call_help,這里相當(dāng)于調(diào)用了call_help(value, method, args, thread)牺弄,因?yàn)閏all_help其實(shí)就是個(gè)函數(shù)指針姻几,同樣定義在JavaCalls中:

void JavaCalls::call_helper(JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS) {
    ......
      StubRoutines::call_stub()(
        (address)&link,
        // (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
        result_val_address,          // see NOTE above (compiler problem)
        result_type,
        method(),
        entry_point,
        args->parameters(),
        args->size_of_parameters(),
        CHECK
      );
    ......
}

可見(jiàn)call_help中最終是通過(guò)StubRoutines::call_stub()的返回值來(lái)調(diào)用java方法的;由此可知势告,call_stub()返回的肯定也是個(gè)函數(shù)指針之類的蛇捌。我們來(lái)看看call_stub()返回的具體是啥:

  /openjdk/hotspot/src/share/vm/runtime/stubRoutines.hpp
  static CallStub call_stub() { return CAST_TO_FN_PTR(CallStub, _call_stub_entry); }

call_stub()返回了_call_stub_entry例程的地址,例程是啥咱台,我開(kāi)始時(shí)也覺(jué)得很難理解络拌,而且“例程”這個(gè)名字也取得很奇怪。其實(shí)例程可以理解為用匯編寫(xiě)好的一個(gè)方法回溺,和內(nèi)聯(lián)匯編差不多春贸,被加載到內(nèi)存中后混萝,我們就可以直接通過(guò)它的首地址來(lái)調(diào)用執(zhí)行它。很多讀者可能也覺(jué)得很奇怪萍恕,為什么要用匯編呢逸嘀?是因?yàn)閰R編快嗎?那C語(yǔ)言寫(xiě)的方法最后不也會(huì)被編譯成匯編嗎允粤,有什么區(qū)別呢崭倘?首先,就是因?yàn)閰R編快类垫,“快”其實(shí)不太準(zhǔn)確司光,C語(yǔ)言雖然也會(huì)被編譯成匯編,最后編譯成二進(jìn)制指令阔挠,但是編譯器生成的C語(yǔ)言指令會(huì)很長(zhǎng)飘庄,有很多冗余的指令脑蠕,而為了實(shí)現(xiàn)同樣一個(gè)功能购撼,程序員自己寫(xiě)的匯編會(huì)比較精簡(jiǎn),指令少谴仙,優(yōu)化多迂求,自然也就更“快”了。

那么_call_stub_entry這個(gè)例程是何時(shí)生成的呢晃跺?答案就在generate_call_stub()中揩局,這個(gè)方法有點(diǎn)長(zhǎng),大家有點(diǎn)耐心掀虎。

下面大家會(huì)看到很多類似匯編指令的代碼凌盯,其實(shí)這些不是指令,而是一個(gè)個(gè)用來(lái)生成匯編指令的方法烹玉。JVM是通過(guò)MacroAssembler來(lái)生成指令的驰怎。我會(huì)將具體的執(zhí)行過(guò)程通過(guò)注釋的方式插入到代碼中

/openjdk/hotspot/src/cpu/x86/vm/stubGenerator_x86_32.cpp

address generate_call_stub(address& return_address) {
    StubCodeMark mark(this, "StubRoutines", "call_stub");
    //匯編器會(huì)將生成的例程在內(nèi)存中線性排列。所以取當(dāng)前匯編器生成的上個(gè)例程最后一行匯編指令的地址二打,用來(lái)作為即將生成的新例程的首地址
    address start = __ pc();

    // stub code parameters / addresses
    assert(frame::entry_frame_call_wrapper_offset == 2, "adjust this code");
    bool  sse_save = false;
    const Address rsp_after_call(rbp, -4 * wordSize); // same as in generate_catch_exception()!
    const int     locals_count_in_bytes  (4*wordSize);

    //定義一些變量县忌,用于保存一些調(diào)用方的信息,這四個(gè)參數(shù)放在被調(diào)用者堆棧中,即call_stub例程堆棧中继效,所以相對(duì)于call_stub例程的椫⑿樱基址(rbp)為負(fù)數(shù)。(棧是向下增長(zhǎng))瑞信,后面會(huì)用到這四個(gè)變量厉颤。
    const Address mxcsr_save    (rbp, -4 * wordSize);
    const Address saved_rbx     (rbp, -3 * wordSize);
    const Address saved_rsi     (rbp, -2 * wordSize);
    const Address saved_rdi     (rbp, -1 * wordSize);
    //傳參,放在調(diào)用方堆棧中凡简,所以相對(duì)call_stub例程的棻朴眩基址為正數(shù),可以理解為調(diào)用方在調(diào)用call_stub例程之前绩郎,會(huì)將傳參都放在自己的堆棧中,這樣call_stub例程中就可以直接基于椢坛眩基址進(jìn)行偏移取用了肋杖。
    const Address result        (rbp,  3 * wordSize);
    const Address result_type   (rbp,  4 * wordSize);
    const Address method        (rbp,  5 * wordSize);
    const Address entry_point   (rbp,  6 * wordSize);
    const Address parameters    (rbp,  7 * wordSize);
    const Address parameter_size(rbp,  8 * wordSize);
    const Address thread        (rbp,  9 * wordSize); // same as in     generate_catch_exception()!
    sse_save =  UseSSE > 0;

    //enter()對(duì)應(yīng)的方法如下,用來(lái)保存調(diào)用方椡诤基址状植,并將call_stub棧基址更新為當(dāng)前棧頂?shù)刂吩勾琧語(yǔ)言編譯器其實(shí)在調(diào)用方法前都會(huì)插入這件事津畸,這里JVM相對(duì)于借用了這種思想。
 ---------------------------------------------
|        void MacroAssembler::enter() {       |
|            push(rbp);                       |
|            mov(rbp, rsp);                   |
|       }                                     |
 ---------------------------------------------
    __ enter();

    //接下來(lái)計(jì)算并分配call_stub堆棧所需棧大小必怜。
    //先將參數(shù)數(shù)量放入rcx寄存器肉拓。
    __ movptr(rcx, parameter_size);              // parameter counter
    //shl用于左移,這里將rcx中的值左移了Interpreter::logStackElementSize位梳庆,在64位平臺(tái)暖途,logStackElementSize=3;在32位平臺(tái)膏执,logStackElementSize=2驻售;所以在64位平臺(tái)上,rcx = rcx * 8, 即每個(gè)參數(shù)占用8字節(jié);32位平臺(tái)rcx = rcx *4 ,即每個(gè)參數(shù)占4個(gè)字節(jié)更米。
    __ shlptr(rcx, Interpreter::logStackElementSize); // convert parameter count to bytes

    // locals_count_in_bytes 在上面有定義:const int     locals_count_in_bytes  (4*wordSize);這四個(gè)字節(jié)其實(shí)就是上面用來(lái)保存調(diào)用方信息所占空間欺栗。
    __ addptr(rcx, locals_count_in_bytes);       // reserve space for register saves

    //rcx現(xiàn)在保存了計(jì)算好的所需棧空間征峦,將保存棧頂?shù)刂返募拇嫫鱮sp減去rcx迟几,即向下擴(kuò)展棧。
    __ subptr(rsp, rcx);

    //引用《揭秘Java虛擬機(jī)》:為了加速內(nèi)存尋址和回收栏笆,物理機(jī)器在分配堆椑嗳空間時(shí)都會(huì)進(jìn)行內(nèi)存對(duì)齊,JVM也借用了這個(gè)思想竖伯。JVM中是按照兩個(gè)字節(jié)存哲,即16位進(jìn)行對(duì)齊的:const int StackAlignmentInBytes = (2*wordSize);
    __ andptr(rsp, -(StackAlignmentInBytes));    // Align stack

    //將調(diào)用方的一些信息,保存到棧中分配的地址處七婴,最后會(huì)再次還原到寄存器中
    __ movtr(saved_rdi, rdi);
    __ movptr(saved_rsi, rsi);
    __ movptr(saved_rbx, rbx);
   ......
   ......
    //接下來(lái)就要進(jìn)行參數(shù)壓棧了;
    Label parameters_done;
    //檢查參數(shù)數(shù)量是否為0祟偷,為0則直接跳到標(biāo)號(hào)parameters_done處。
    __ movl(rcx, parameter_size);  // parameter counter
    __ testl(rcx, rcx);
    __ jcc(Assembler::zero, parameters_done);

    Label loop
    //將參數(shù)首地址放到寄存器rdx中打厘,并將rbx置0修肠;
    __ movptr(rdx, parameters);          // parameter pointer
    __ xorptr(rbx, rbx);

    //標(biāo)號(hào)loop處
    __ BIND(loop);

    //此處開(kāi)始循環(huán);從最后一個(gè)參數(shù)倒序往前進(jìn)行參數(shù)壓棧户盯,初始時(shí)嵌施,rcx = parameter_size饲化;要注意,這里的參數(shù)是指java方法所需的參數(shù)吗伤,而不是call_stub例程所需參數(shù)期升!
    //將(rdx + rcx * stackElementScale()- wordSize )移到 rax 中喻鳄,(rdx + rcx * stackElementScale()- wordSize )指向了要壓棧的參數(shù)节仿。
    __ movptr(rax, Address(rdx, rcx, Interpreter::stackElementScale(), -wordSize));
    //再?gòu)膔ax中轉(zhuǎn)移到(rsp + rbx * stackElementScale()) 處丧裁,expr_offset_in_bytes(0) = 0;這里是基于棧頂?shù)刂愤M(jìn)行偏移尋址的巧号,最后一個(gè)參數(shù)會(huì)被壓到棧頂處族奢。第一個(gè)參數(shù)會(huì)被壓到rsp + (parameter_size-1)* stackElementScale()處。
    __ movptr(Address(rsp, rbx, Interpreter::stackElementScale(),
                Interpreter::expr_offset_in_bytes(0)), rax);          // store parameter
    //更新rbx
    __ increment(rbx);
    //自減rcx丹鸿,當(dāng)rcx不為0時(shí)越走,繼續(xù)跳往loop處循環(huán)執(zhí)行。
    __ decrement(rcx);
    __ jcc(Assembler::notZero, loop);

    //標(biāo)號(hào)parameters_done處
    __ BIND(parameters_done);

   //接下來(lái)要開(kāi)始調(diào)用Java方法了靠欢。
   //將調(diào)用java方法的entry_point例程所需的一些參數(shù)保存到寄存器中
    __ movptr(rbx, method);           // get Method*
    __ movptr(rax, entry_point);      // get entry_point
    __ mov(rsi, rsp);                 // set sender sp
   //跳往entry_point例程執(zhí)行
    __ call(rax);

    ......
}

二:EntryPoint例程

上面最后會(huì)跳往entry_point例程執(zhí)行廊敌,現(xiàn)在有個(gè)新的問(wèn)題,entry_point例程是個(gè)啥掺涛?其實(shí)entry_point例程和call_stub例程一樣庭敦,都是用匯編寫(xiě)的來(lái)執(zhí)行java方法的工具。

我們回到JavaCalls::call_helper()中:

address entry_point = method->from_interpreted_entry();

entry_point是從當(dāng)前要執(zhí)行的Java方法中獲取的:

  /openjdk/hotspot/src/share/vm/oops/method.hpp

  volatile address from_interpreted_entry() const{ 
      return (address)OrderAccess::load_ptr_acquire(&_from_interpreted_entry); 
  }

那么_from_interpreted_entry是何時(shí)賦值的薪缆?method.hpp中有這樣一個(gè)set方法:

void set_interpreter_entry(address entry)      { 
    _i2i_entry = entry;  
    _from_interpreted_entry = entry; 
}

我們來(lái)看看是何時(shí)調(diào)用了method的這個(gè)set方法:

// Called when the method_holder is getting linked. Setup entrypoints so the method
// is ready to be called from interpreter, compiler, and vtables.
void Method::link_method(methodHandle h_method, TRAPS) {
  ......
  address entry = Interpreter::entry_for_method(h_method);
  assert(entry != NULL, "interpreter entry must be non-null");
  // Sets both _i2i_entry and _from_interpreted_entry
  set_interpreter_entry(entry);
  ......
}

根據(jù)注釋都可以得知,當(dāng)方法鏈接時(shí)伞广,會(huì)去設(shè)置方法的entry_point拣帽,entry_point是由Interpreter::entry_for_method(h_method)得到的:

  static address    entry_for_method(methodHandle m)            { return entry_for_kind(method_kind(m)); }

首先通過(guò)method_kind()拿到方法類型,接著調(diào)用entry_for_kind():

  static address    entry_for_kind(MethodKind k){ 
      return _entry_table[k]; 
  }

這里直接返回了_entry_table數(shù)組中對(duì)應(yīng)方法類型索引的entry_point地址嚼锄。給數(shù)組中元素賦值專門(mén)有個(gè)方法:

void AbstractInterpreter::set_entry_for_kind(AbstractInterpreter::MethodKind kind, address entry) {
  _entry_table[kind] = entry;
}

那么何時(shí)會(huì)調(diào)用set_entry_for_kind()呢减拭,答案就在TemplateInterpreterGenerator::generate_all()中,generate_all()會(huì)調(diào)用generate_method_entry()去生成每種方法的entry_point区丑,所有Java方法的執(zhí)行拧粪,都會(huì)通過(guò)對(duì)應(yīng)類型的entry_point例程來(lái)輔助。

現(xiàn)在就豁然開(kāi)朗了沧侥,調(diào)用Java方法時(shí)可霎,首先通過(guò)method找到對(duì)應(yīng)的entry_point例程,并傳遞給call_stub例程宴杀,call_stub準(zhǔn)備好堆棧后癣朗,就開(kāi)始前往entry_point處,entry_point例程就會(huì)開(kāi)始執(zhí)行傳遞給它的Java方法了旺罢。在研究JVM時(shí)旷余,我們不要把Java方法當(dāng)作方法绢记,要把它當(dāng)作一個(gè)對(duì)象來(lái)對(duì)待,下次有時(shí)間在好好研究下entry_point例程正卧。

好了蠢熄,不多說(shuō)了,放鞭炮去了炉旷,新年快樂(lè)护赊!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市砾跃,隨后出現(xiàn)的幾起案子骏啰,更是在濱河造成了極大的恐慌,老刑警劉巖抽高,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件判耕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡翘骂,警方通過(guò)查閱死者的電腦和手機(jī)壁熄,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)碳竟,“玉大人草丧,你說(shuō)我怎么就攤上這事∮ㄎΓ” “怎么了昌执?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)诈泼。 經(jīng)常有香客問(wèn)我懂拾,道長(zhǎng),這世上最難降的妖魔是什么铐达? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任岖赋,我火速辦了婚禮,結(jié)果婚禮上瓮孙,老公的妹妹穿的比我還像新娘唐断。我一直安慰自己,他們只是感情好杭抠,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布脸甘。 她就那樣靜靜地躺著,像睡著了一般祈争。 火紅的嫁衣襯著肌膚如雪斤程。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音忿墅,去河邊找鬼扁藕。 笑死,一個(gè)胖子當(dāng)著我的面吹牛疚脐,可吹牛的內(nèi)容都是我干的亿柑。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼棍弄,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼望薄!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起呼畸,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤痕支,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蛮原,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體卧须,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年儒陨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了花嘶。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蹦漠,死狀恐怖椭员,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情笛园,我是刑警寧澤隘击,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站喘沿,受9級(jí)特大地震影響闸度,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蚜印,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望留量。 院中可真熱鬧窄赋,春花似錦、人聲如沸楼熄。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)可岂。三九已至错敢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背稚茅。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工纸淮, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人亚享。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓咽块,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親欺税。 傳聞我的和親對(duì)象是個(gè)殘疾皇子侈沪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)晚凿,斷路器亭罪,智...
    卡卡羅2017閱讀 134,601評(píng)論 18 139
  • 如何徹底研究透一家公司 2018-02-15 實(shí)戰(zhàn)財(cái)經(jīng) 持續(xù)獲取信息,請(qǐng)點(diǎn)擊上方"實(shí)戰(zhàn)財(cái)經(jīng)"關(guān)注 第一部分:公司“...
    稅苑原稅閱讀 171評(píng)論 0 0
  • 1 雞年 臘月二十五的時(shí)候歼秽,已臨近過(guò)年应役,我在初高組魔法訓(xùn)練營(yíng),有了相對(duì)充足的時(shí)間哲银,屬于自己扛吞,可以做自己喜歡做的事,...
    一池云錦丹閱讀 238評(píng)論 0 1
  • 品牌的背后是代表消費(fèi)者的一個(gè)選擇和信任荆责,現(xiàn)在人們生活質(zhì)量一直在提高滥比,對(duì)于選擇品牌性有了一定的要求,因?yàn)楝F(xiàn)在讀認(rèn)識(shí)到...
    楊老頭說(shuō)事閱讀 134評(píng)論 0 2
  • 倘若讓我闡述總結(jié)一下個(gè)人的人生,價(jià)值键耕,世界觀 我覺(jué)得是在讓我講一個(gè)僅供娛樂(lè)的段子 我本身就是一只來(lái)至三觀不正吐槽星...
    壞心眼的淑女閱讀 365評(píng)論 0 0