趁著春節(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è)护赊!