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源码剖析系列文章!