ldc指令將int、float、或者一個類、方法型別或方法控制代碼的符號引用、還可能是String型常量值從常量池中推送至棧頂。
這一篇介紹一個虛擬機器規範中定義的一個位元組碼指令ldc,另外還有一個虛擬機器內部使用的位元組碼指令_fast_aldc。ldc指令可以載入String、方法型別或方法控制代碼的符號引用,但是如果要載入String、方法型別或方法控制代碼的符號引用,則會在類連線過程中重寫ldc位元組碼指令為虛擬機器內部使用的位元組碼指令_fast_aldc。下面我們詳細介紹ldc指令如何載入int、float型別和類型別的資料,以及_fast_aldc載入String、方法型別或方法控制代碼,還有為什麼要進行位元組碼重寫等問題。
1、ldc位元組碼指令
ldc指令將int、float或String型常量值從常量池中推送至棧頂。模板的定義如下:
def(Bytecodes::_ldc , ubcp|____|clvm|____, vtos, vtos, ldc , false );
ldc位元組碼指令的格式如下:
// index是一個無符號的byte型別資料,指明當前類的執行時常量池的索引
ldc index
呼叫生成函式TemplateTable::ldc(bool wide)。函式生成的彙編程式碼如下:
第1部分程式碼:
// movzbl指令負責拷貝一個位元組,並用0填充其目
// 的運算元中的其餘各位,這種擴充套件方式叫"零擴充套件"
// ldc指定的格式為ldc index,index為一個位元組
0x00007fffe1028530: movzbl 0x1(%r13),%ebx // 載入index到%ebx // %rcx指向快取池首地址、%rax指向型別陣列_tags首地址
0x00007fffe1028535: mov -0x18(%rbp),%rcx
0x00007fffe1028539: mov 0x10(%rcx),%rcx
0x00007fffe102853d: mov 0x8(%rcx),%rcx
0x00007fffe1028541: mov 0x10(%rcx),%rax // 從_tags陣列獲取運算元型別並存儲到%edx中
0x00007fffe1028545: movzbl 0x4(%rax,%rbx,1),%edx // $0x64代表JVM_CONSTANT_UnresolvedClass,比較,如果類還沒有連結,
// 則直接跳轉到call_ldc
0x00007fffe102854a: cmp $0x64,%edx
0x00007fffe102854d: je 0x00007fffe102855d // call_ldc // $0x67代表JVM_CONSTANT_UnresolvedClassInError,也就是如果類在
// 連結過程中出現錯誤,則跳轉到call_ldc
0x00007fffe102854f: cmp $0x67,%edx
0x00007fffe1028552: je 0x00007fffe102855d // call_ldc // $0x7代表JVM_CONSTANT_Class,表示如果類已經進行了連線,則
// 跳轉到notClass
0x00007fffe1028554: cmp $0x7,%edx
0x00007fffe1028557: jne 0x00007fffe10287c0 // notClass // 類在沒有連線或連線過程中出錯,則執行如下的彙編程式碼
// -- call_ldc --
下面看一下呼叫call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::ldc), c_rarg1)函式生成的彙編程式碼,CAST_FROM_FN_PTR是巨集,巨集擴充套件後為( (address)((address_word)(InterpreterRuntime::ldc)) )。
在呼叫call_VM()函式時,傳遞的引數如下:
- %rax現在儲存型別陣列首地址,不過傳入是為了接收呼叫函式的結果值
- adr是InterpreterRuntime::ldc()函式首地址
- c_rarg1用rdi暫存器儲存wide值,這裡為0,表示為沒有加wide字首的ldc指令生成彙編程式碼
生成的彙編程式碼如下:
第2部分:
// 將wide的值移到%esi暫存器,為後續
// 呼叫InterpreterRuntime::ldc()函式準備第2個引數
0x00007fffe102855d: mov $0x0,%esi
// 呼叫MacroAssembler::call_VM()函式,通過此函式來呼叫HotSpot VM中用
// C++編寫的函式,通過這個C++編寫的函式來呼叫InterpreterRuntime::ldc()函式 0x00007fffe1017542: callq 0x00007fffe101754c
0x00007fffe1017547: jmpq 0x00007fffe10175df // 跳轉到E1 // 呼叫MacroAssembler::call_VM_helper()函式
// 將棧頂儲存的返回地址設定到%rax中,也就是將儲存地址0x00007fffe1017547
// 的棧的slot地址設定到%rax中
0x00007fffe101754c: lea 0x8(%rsp),%rax // 呼叫InterpreterMacroAssembler::call_VM_base()函式
// 儲存bcp到棧中特定位置
0x00007fffe1017551: mov %r13,-0x38(%rbp) // 呼叫MacroAssembler::call_VM_base()函式
// 將r15中的值移動到rdi暫存器中,也就是為函式呼叫準備第一個引數
0x00007fffe1017555: mov %r15,%rdi
// 只有直譯器才必須要設定fp
// 將last_java_fp儲存到JavaThread類的last_java_fp屬性中
0x00007fffe1017558: mov %rbp,0x200(%r15)
// 將last_java_sp儲存到JavaThread類的last_java_sp屬性中
0x00007fffe101755f: mov %rax,0x1f0(%r15) // ... 省略呼叫MacroAssembler::call_VM_leaf_base()函式 // 重置JavaThread::last_java_sp與JavaThread::last_java_fp屬性的值
0x00007fffe1017589: movabs $0x0,%r10
0x00007fffe1017593: mov %r10,0x1f0(%r15)
0x00007fffe101759a: movabs $0x0,%r10
0x00007fffe10175a4: mov %r10,0x200(%r15) // check for pending exceptions (java_thread is set upon return)
0x00007fffe10175ab: cmpq $0x0,0x8(%r15)
// 如果沒有異常則直接跳轉到ok
0x00007fffe10175b3: je 0x00007fffe10175be
// 如果有異常則跳轉到StubRoutines::forward_exception_entry()獲取的例程入口
0x00007fffe10175b9: jmpq 0x00007fffe1000420 // -- ok --
// 將JavaThread::vm_result屬性中的值儲存到%rax暫存器中並清空vm_result屬性的值
0x00007fffe10175be: mov 0x250(%r15),%rax
0x00007fffe10175c5: movabs $0x0,%r10
0x00007fffe10175cf: mov %r10,0x250(%r15) // 結束呼叫MacroAssembler::call_VM_base()函式 // 恢復bcp與locals
0x00007fffe10175d6: mov -0x38(%rbp),%r13
0x00007fffe10175da: mov -0x30(%rbp),%r14 // 結束呼叫MacroAssembler::call_VM_helper()函式 0x00007fffe10175de: retq
// 結束呼叫MacroAssembler::call_VM()函式
下面詳細解釋如下彙編的意思。
call指令相當於如下兩條指令:
push %eip
jmp addr
而ret指令相當於:
pop %eip
所以如上彙編程式碼:
0x00007fffe1017542: callq 0x00007fffe101754c
0x00007fffe1017547: jmpq 0x00007fffe10175df // 跳轉
...
0x00007fffe10175de: retq
呼叫callq指令將jmpq的地址壓入了表示式棧,也就是壓入了返回地址x00007fffe1017547,這樣當後續呼叫retq時,會跳轉到jmpq指令執行,而jmpq又跳轉到了0x00007fffe10175df地址處的指令執行。
通過呼叫MacroAssembler::call_VM()函式來呼叫HotSpot VM中用的C++編寫的函式,call_VM()函式還會呼叫如下函式:
MacroAssembler::call_VM_helper
InterpreterMacroAssembler::call_VM_base()
MacroAssembler::call_VM_base()
MacroAssembler::call_VM_leaf_base()
在如上幾個函式中,最重要的就是在MacroAssembler::call_VM_base()函式中儲存rsp、rbp的值到JavaThread::last_java_sp與JavaThread::last_java_fp屬性中,然後通過MacroAssembler::call_VM_leaf_base()函式生成的彙編程式碼來呼叫C++編寫的InterpreterRuntime::ldc()函式,如果呼叫InterpreterRuntime::ldc()函式有可能破壞rsp和rbp的值(其它的%r13、%r14等的暫存器中的值也有可能破壞,所以在必要時儲存到棧中,在呼叫完成後再恢復,這樣這些暫存器其實就算的上是呼叫者儲存的暫存器了),所以為了保證rsp、rbp,將這兩個值儲存到執行緒中,線上程中儲存的這2個值對於棧展開非常非常重要,後面我們會詳細介紹。
由於如上彙編程式碼會解釋執行,在解釋執行過程中會呼叫C++函式,所以C/C++棧和Java棧都混在一起,這為我們查詢帶來了一定的複雜度。
呼叫的MacroAssembler::call_VM_leaf_base()函式生成的彙編程式碼如下:
第3部分彙編程式碼:
// 呼叫MacroAssembler::call_VM_leaf_base()函式
0x00007fffe1017566: test $0xf,%esp // 檢查對齊
// %esp對齊的操作,跳轉到 L
0x00007fffe101756c: je 0x00007fffe1017584
// %esp沒有對齊時的操作
0x00007fffe1017572: sub $0x8,%rsp
0x00007fffe1017576: callq 0x00007ffff66a22a2 // 呼叫函式,也就是呼叫InterpreterRuntime::ldc()函式
0x00007fffe101757b: add $0x8,%rsp
0x00007fffe101757f: jmpq 0x00007fffe1017589 // 跳轉到E2
// -- L --
// %esp對齊的操作
0x00007fffe1017584: callq 0x00007ffff66a22a2 // 呼叫函式,也就是呼叫InterpreterRuntime::ldc()函式 // -- E2 -- // 結束呼叫
MacroAssembler::call_VM_leaf_base()函式
在如上這段彙編中會真正呼叫C++函式InterpreterRuntime::ldc(),由於這是一個C++函式,所以在呼叫時,如果要傳遞引數,則要遵守C++呼叫約定,也就是前6個引數都放到固定的暫存器中。這個函式需要2個引數,分別為thread和wide,已經分別放到了%rdi和%rax暫存器中了。InterpreterRuntime::ldc()函式的實現如下:
// ldc負責將數值常量或String常量值從常量池中推送到棧頂
IRT_ENTRY(void, InterpreterRuntime::ldc(JavaThread* thread, bool wide))
ConstantPool* pool = method(thread)->constants();
int index = wide ? get_index_u2(thread, Bytecodes::_ldc_w) : get_index_u1(thread, Bytecodes::_ldc);
constantTag tag = pool->tag_at(index); Klass* klass = pool->klass_at(index, CHECK);
oop java_class = klass->java_mirror(); // java.lang.Class通過oop來表示
thread->set_vm_result(java_class);
IRT_END
函式將查詢到的、當前正在解釋執行的方法所屬的類儲存到JavaThread類的vm_result屬性中。我們可以回看第2部分彙編程式碼,會將vm_result屬性的值設定到%rax中。
接下來繼續看TemplateTable::ldc(bool wide)函式生成的彙編程式碼,此時已經通過呼叫call_VM()函式生成了呼叫InterpreterRuntime::ldc()這個C++的彙編,呼叫完成後值已經放到了%rax中。
// -- E1 --
0x00007fffe10287ba: push %rax // 將呼叫的結果儲存到表示式中
0x00007fffe10287bb: jmpq 0x00007fffe102885e // 跳轉到Done // -- notClass --
// $0x4表示JVM_CONSTANT_Float
0x00007fffe10287c0: cmp $0x4,%edx
0x00007fffe10287c3: jne 0x00007fffe10287d9 // 跳到notFloat
// 當ldc位元組碼指令載入的數為float時執行如下彙編程式碼
0x00007fffe10287c5: vmovss 0x58(%rcx,%rbx,8),%xmm0
0x00007fffe10287cb: sub $0x8,%rsp
0x00007fffe10287cf: vmovss %xmm0,(%rsp)
0x00007fffe10287d4: jmpq 0x00007fffe102885e // 跳轉到Done // -- notFloat --
// 當ldc位元組碼指令載入的為非float,也就是int型別資料時通過push加入表示式棧
0x00007fffe1028859: mov 0x58(%rcx,%rbx,8),%eax
0x00007fffe102885d: push %rax // -- Done --
由於ldc指令除了載入String外,還可能載入int和float,如果是int,直接呼叫push壓入表示式棧中,如果是float,則在表示式棧上開闢空間,然後移到到這個開闢的slot中儲存。注意,float會使用%xmm0暫存器。
2、fast_aldc虛擬機器內部位元組碼指令
下面介紹_fast_aldc指令,這個指令是虛擬機器內部使用的指令而非虛擬機器規範定義的指令。_fast_aldc指令的模板定義如下:
def(Bytecodes::_fast_aldc , ubcp|____|clvm|____, vtos, atos, fast_aldc , false );
生成函式為TemplateTable::fast_aldc(bool wide),這個函式生成的彙編程式碼如下:
// 呼叫InterpreterMacroAssembler::get_cache_index_at_bcp()函式生成
// 獲取位元組碼指令的運算元,這個運算元已經指向了常量池快取項的索引,在位元組碼重寫
// 階段已經進行了位元組碼重寫
0x00007fffe10243d0: movzbl 0x1(%r13),%edx // 呼叫InterpreterMacroAssembler::load_resolved_reference_at_index()函式生成 // shl表示邏輯左移,相當於乘4,因為ConstantPoolCacheEntry的大小為4個字
0x00007fffe10243d5: shl $0x2,%edx // 獲取Method*
0x00007fffe10243d8: mov -0x18(%rbp),%rax
// 獲取ConstMethod*
0x00007fffe10243dc: mov 0x10(%rax),%rax
// 獲取ConstantPool*
0x00007fffe10243e0: mov 0x8(%rax),%rax
// 獲取ConstantPool::_resolved_references屬性的值,這個值
// 是一個指向物件陣列的指標
0x00007fffe10243e4: mov 0x30(%rax),%rax // JNIHandles::resolve(obj)
0x00007fffe10243e8: mov (%rax),%rax // 從_resolved_references陣列指定的下標索引處獲取oop,先進行索引偏移
0x00007fffe10243eb: add %rdx,%rax // 要在%rax上加0x10,是因為陣列物件的頭大小為2個字,加上後
// %rax就指向了oop
0x00007fffe10243ee: mov 0x10(%rax),%eax
獲取_resolved_references屬性的值,涉及到的2個屬性在ConstantPool類中的定義如下:
// Array of resolved objects from the constant pool and map from resolved
// object index to original constant pool index
jobject _resolved_references; // jobject是指標型別
Array<u2>* _reference_map;
關於_resolved_references指向的其實是Object陣列。在ConstantPool::initialize_resolved_references()函式中初始化這個屬性。呼叫鏈如下:
ConstantPool::initialize_resolved_references() constantPool.cpp
Rewriter::make_constant_pool_cache() rewriter.cpp
Rewriter::Rewriter() rewriter.cpp
Rewriter::rewrite() rewriter.cpp
InstanceKlass::rewrite_class() instanceKlass.cpp
InstanceKlass::link_class_impl() instanceKlass.cpp
後續如果需要連線ldc等指令時,可能會呼叫如下函式:(我們只討論ldc載入String型別資料的問題,所以我們只看往_resolved_references屬性中放入表示String的oop的邏輯,MethodType與MethodHandle將不再介紹,有興趣的可自行研究)
oop ConstantPool::string_at_impl(
constantPoolHandle this_oop,
int which,
int obj_index,
TRAPS
) {
oop str = this_oop->resolved_references()->obj_at(obj_index);
if (str != NULL)
return str; Symbol* sym = this_oop->unresolved_string_at(which);
str = StringTable::intern(sym, CHECK_(NULL)); this_oop->string_at_put(which, obj_index, str); return str;
} void string_at_put(int which, int obj_index, oop str) {
// 獲取型別為jobject的_resolved_references屬性的值
objArrayOop tmp = resolved_references();
tmp->obj_at_put(obj_index, str);
}
在如上函式中向_resolved_references陣列中設定快取的值。
大概的思路就是:如果ldc載入的是字串,那麼儘量通過_resolved_references陣列中一次性找到表示字串的oop,否則要通過原常量池下標索引找到Symbol例項(Symbol例項是HotSpot VM內部使用的、用來表示字串),根據Symbol例項生成對應的oop,然後通過常量池快取下標索引設定到_resolved_references中。當下次查詢時,通過這個常量池快取下標快取找到表示字串的oop。
獲取到_resolved_references屬性的值後接著看生成的彙編程式碼,如下:
// ...
// %eax中儲存著表示字串的oop
0x00007fffe1024479: test %eax,%eax
// 如果已經獲取到了oop,則跳轉到resolved
0x00007fffe102447b: jne 0x00007fffe1024481 // 沒有獲取到oop,需要進行連線操作,0xe5是_fast_aldc的Opcode
0x00007fffe1024481: mov $0xe5,%edx
呼叫call_VM()函式生成的彙編程式碼如下:
// 呼叫InterpreterRuntime::resolve_ldc()函式
0x00007fffe1024486: callq 0x00007fffe1024490
0x00007fffe102448b: jmpq 0x00007fffe1024526 // 將%rdx中的ConstantPoolCacheEntry項儲存到第1個引數中 // 呼叫MacroAssembler::call_VM_helper()函式生成
0x00007fffe1024490: mov %rdx,%rsi
// 將返回地址載入到%rax中
0x00007fffe1024493: lea 0x8(%rsp),%rax // 呼叫call_VM_base()函式生成
// 儲存bcp
0x00007fffe1024498: mov %r13,-0x38(%rbp) // 呼叫MacroAssembler::call_VM_base()函式生成 // 將r15中的值移動到c_rarg0(rdi)暫存器中,也就是為函式呼叫準備第一個引數
0x00007fffe102449c: mov %r15,%rdi
// Only interpreter should have to set fp 只有直譯器才必須要設定fp
0x00007fffe102449f: mov %rbp,0x200(%r15)
0x00007fffe10244a6: mov %rax,0x1f0(%r15) // 呼叫MacroAssembler::call_VM_leaf_base()生成
0x00007fffe10244ad: test $0xf,%esp
0x00007fffe10244b3: je 0x00007fffe10244cb
0x00007fffe10244b9: sub $0x8,%rsp
0x00007fffe10244bd: callq 0x00007ffff66b27ac
0x00007fffe10244c2: add $0x8,%rsp
0x00007fffe10244c6: jmpq 0x00007fffe10244d0
0x00007fffe10244cb: callq 0x00007ffff66b27ac
0x00007fffe10244d0: movabs $0x0,%r10
// 結束呼叫MacroAssembler::call_VM_leaf_base() 0x00007fffe10244da: mov %r10,0x1f0(%r15)
0x00007fffe10244e1: movabs $0x0,%r10 // 檢查是否有異常發生
0x00007fffe10244eb: mov %r10,0x200(%r15)
0x00007fffe10244f2: cmpq $0x0,0x8(%r15)
// 如果沒有異常發生,則跳轉到ok
0x00007fffe10244fa: je 0x00007fffe1024505
// 有異常發生,則跳轉到StubRoutines::forward_exception_entry()
0x00007fffe1024500: jmpq 0x00007fffe1000420 // ---- ok ---- // 將JavaThread::vm_result屬性中的值儲存到oop_result暫存器中並清空vm_result屬性的值
0x00007fffe1024505: mov 0x250(%r15),%rax
0x00007fffe102450c: movabs $0x0,%r10
0x00007fffe1024516: mov %r10,0x250(%r15) // 結果呼叫MacroAssembler::call_VM_base()函式 // 恢復bcp和locals
0x00007fffe102451d: mov -0x38(%rbp),%r13
0x00007fffe1024521: mov -0x30(%rbp),%r14 // 結束呼叫InterpreterMacroAssembler::call_VM_base()函式
// 結束呼叫MacroAssembler::call_VM_helper()函式 0x00007fffe1024525: retq // 結束呼叫MacroAssembler::call_VM()函式,回到
// TemplateTable::fast_aldc()函式繼續看生成的程式碼,只
// 定義了resolved點 // ---- resolved ----
呼叫的InterpreterRuntime::resolve_ldc()函式的實現如下:
IRT_ENTRY(void, InterpreterRuntime::resolve_ldc(
JavaThread* thread,
Bytecodes::Code bytecode)
) {
ResourceMark rm(thread);
methodHandle m (thread, method(thread));
Bytecode_loadconstant ldc(m, bci(thread));
oop result = ldc.resolve_constant(CHECK); thread->set_vm_result(result);
}
IRT_END
這個函式會呼叫一系列的函式,相關呼叫鏈如下:
ConstantPool::string_at_put() constantPool.hpp
ConstantPool::string_at_impl() constantPool.cpp
ConstantPool::resolve_constant_at_impl() constantPool.cpp
ConstantPool::resolve_cached_constant_at() constantPool.hpp
Bytecode_loadconstant::resolve_constant() bytecode.cpp
InterpreterRuntime::resolve_ldc() interpreterRuntime.cpp
其中ConstantPool::string_at_impl()函式在前面已經詳細介紹過。
呼叫的resolve_constant()函式的實現如下:
oop Bytecode_loadconstant::resolve_constant(TRAPS) const {
int index = raw_index();
ConstantPool* constants = _method->constants();
if (has_cache_index()) {
return constants->resolve_cached_constant_at(index, THREAD);
} else {
return constants->resolve_constant_at(index, THREAD);
}
}
呼叫的resolve_cached_constant_at()或resolve_constant_at()函式的實現如下:
oop resolve_cached_constant_at(int cache_index, TRAPS) {
constantPoolHandle h_this(THREAD, this);
return resolve_constant_at_impl(h_this, _no_index_sentinel, cache_index, THREAD);
} oop resolve_possibly_cached_constant_at(int pool_index, TRAPS) {
constantPoolHandle h_this(THREAD, this);
return resolve_constant_at_impl(h_this, pool_index, _possible_index_sentinel, THREAD);
}
呼叫的resolve_constant_at_impl()函式的實現如下:
oop ConstantPool::resolve_constant_at_impl(
constantPoolHandle this_oop,
int index,
int cache_index,
TRAPS
) {
oop result_oop = NULL;
Handle throw_exception; if (cache_index == _possible_index_sentinel) {
cache_index = this_oop->cp_to_object_index(index);
} if (cache_index >= 0) {
result_oop = this_oop->resolved_references()->obj_at(cache_index);
if (result_oop != NULL) {
return result_oop;
}
index = this_oop->object_to_cp_index(cache_index);
} jvalue prim_value; // temp used only in a few cases below int tag_value = this_oop->tag_at(index).value(); switch (tag_value) {
// ...
case JVM_CONSTANT_String:
assert(cache_index != _no_index_sentinel, "should have been set");
if (this_oop->is_pseudo_string_at(index)) {
result_oop = this_oop->pseudo_string_at(index, cache_index);
break;
}
result_oop = string_at_impl(this_oop, index, cache_index, CHECK_NULL);
break;
// ...
} if (cache_index >= 0) {
Handle result_handle(THREAD, result_oop);
MonitorLockerEx ml(this_oop->lock());
oop result = this_oop->resolved_references()->obj_at(cache_index);
if (result == NULL) {
this_oop->resolved_references()->obj_at_put(cache_index, result_handle());
return result_handle();
} else {
return result;
}
} else {
return result_oop;
}
}
通過常量池的tags陣列判斷,如果常量池下標index處儲存的是JVM_CONSTANT_String常量池項,則呼叫string_at_impl()函式,這個函式在之前已經介紹過,會根據表示字串的Symbol例項創建出表示字串的oop。在ConstantPool::resolve_constant_at_impl()函式中得到oop後就儲存到ConstantPool::_resolved_references屬性中,最後返回這個oop,這正是ldc需要的oop。
通過重寫fast_aldc位元組碼指令,達到了通過少量指令就直接獲取到oop的目的,而且oop是快取的,所以字串常量在HotSpot VM中的表示唯一,也就是隻有一個oop表示。
C++函式約定返回的值會儲存到%rax中,根據_fast_aldc位元組碼指令的模板定義可知,tos_out為atos,所以後續並不需要進一步操作。
HotSpot VM會在類的連線過程中重寫某些位元組碼,如ldc位元組碼重寫為fast_aldc,還有常量池的tags型別陣列、常量池快取等內容在《深入剖析Java虛擬機器:原始碼剖析與例項詳解》中詳細介紹過,這裡不再介紹。
推薦閱讀:
第2篇-JVM虛擬機器這樣來呼叫Java主類的main()方法
第13篇-通過InterpreterCodelet儲存機器指令片段
如果有問題可直接評論留言或加作者微信mazhimazh
關注公眾號,有HotSpot VM原始碼剖析系列文章!