第20篇-加載與存儲指令之ldc與_fast_aldc指令(2)
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虛拟機:源碼剖析與執行個體詳解》中詳細介紹過,這裡不再介紹。
推薦閱讀:
第1篇-關于JVM運作時,開篇說的簡單些
第2篇-JVM虛拟機這樣來調用Java主類的main()方法
第3篇-CallStub新棧幀的建立
第4篇-JVM終于開始調用Java主類的main()方法啦
第5篇-調用Java方法後彈出棧幀及處理傳回結果
第6篇-Java方法新棧幀的建立
第7篇-為Java方法建立棧幀
第8篇-dispatch_next()函數分派位元組碼
第9篇-位元組碼指令的定義
第10篇-初始化模闆表
第11篇-認識Stub與StubQueue
第12篇-認識CodeletMark
第13篇-通過InterpreterCodelet存儲機器指令片段
第14篇-生成重要的例程
第15章-解釋器及解釋器生成器
第16章-虛拟機中的彙編器
第17章-x86-64寄存器
第18章-x86指令集之常用指令
第19篇-加載與存儲指令(1)
如果有問題可直接評論留言或加作者微信mazhimazh
關注公衆号,有HotSpot VM源碼剖析系列文章!
