Java虚拟机规范中定义的对象操作相关的字节码指令如下表所示。
0xb2 | getstatic | 获取指定类的静态域,并将其值压入栈顶 |
0xb3 | putstatic | 为指定的类的静态域赋值 |
0xb4 | getfield | 获取指定类的实例域,并将其值压入栈顶 |
0xb5 | putfield | 为指定的类的实例域赋值 |
0xbb | new | 创建一个对象,并将其引用值压入栈顶 |
0xbc | newarray | 创建一个指定原始类型(如int,、float,、char等)的数组,并将其引用值压入栈顶 |
0xbd | anewarray | 创建一个引用型(如类、接口或数组)的数组,并将其引用值压入栈顶 |
0xbe | arraylength | 获得数组的长度值并压入栈顶 |
0xc0 | checkcast | 检验类型转换,检验未通过将抛出ClassCastException |
0xc1 | instanceof | 检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶 |
0xc5 | multianewarray | 创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶 |
字节码指令的模板定义如下:
def(Bytecodes::_getstatic , ubcp|____|clvm|____, vtos, vtos, getstatic , f1_byte ); def(Bytecodes::_putstatic , ubcp|____|clvm|____, vtos, vtos, putstatic , f2_byte ); def(Bytecodes::_getfield , ubcp|____|clvm|____, vtos, vtos, getfield , f1_byte ); def(Bytecodes::_putfield , ubcp|____|clvm|____, vtos, vtos, putfield , f2_byte ); def(Bytecodes::_new , ubcp|____|clvm|____, vtos, atos, _new , _ ); def(Bytecodes::_newarray , ubcp|____|clvm|____, itos, atos, newarray , _ ); def(Bytecodes::_anewarray , ubcp|____|clvm|____, itos, atos, anewarray , _ ); def(Bytecodes::_multianewarray , ubcp|____|clvm|____, vtos, atos, multianewarray , _ ); def(Bytecodes::_arraylength , ____|____|____|____, atos, itos, arraylength , _ ); def(Bytecodes::_checkcast , ubcp|____|clvm|____, atos, atos, checkcast , _ ); def(Bytecodes::_instanceof , ubcp|____|clvm|____, atos, itos, instanceof , _ );
new字节码指令的生成函数为TemplateTable::_new(),这在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》的第9章类对象创建时详细介绍过,这里不再介绍。
getstatic字节码指令获取指定类的静态域,并将其值压入栈顶。格式如下:
getstatic indexbyte1 indexbyte2
无符号数indexbyte1和indexbyte2构建为(indexbyte1<<8)|indexbyte2,这个值指明了一个当前类的运行时常量池索引值,指向的运行时常量池项为一个字段的符号引用。
getstatic字节码指令的生成函数为TemplateTable::getstatic(),还有个类似的getfield指令,这些生成函数如下:
void TemplateTable::getfield(int byte_no) { getfield_or_static(byte_no, false); // getfield的byte_no值为1 } void TemplateTable::getstatic(int byte_no) { getfield_or_static(byte_no, true); // getstatic的byte_no的值为1 }
最终都会调用getfield_or_static()函数生成机器指令片段。此函数生成的机器指令片段对应的汇编代码如下:
// 获取ConstantPoolCache中ConstantPoolCacheEntry的index 0x00007fffe101fd10: movzwl 0x1(%r13),%edx // 从栈中获取ConstantPoolCache的首地址 0x00007fffe101fd15: mov -0x28(%rbp),%rcx // 左移2位,因为%edx中存储的是ConstantPoolCacheEntry index, // 左移2位是因为ConstantPoolCacheEntry的内存占用是4个字 0x00007fffe101fd19: shl $0x2,%edx // 计算%rcx+%rdx*8+0x10,获取ConstantPoolCacheEntry[_indices,_f1,_f2,_flags]中的_indices // 因为ConstantPoolCache的大小为0x16字节,%rcx+0x10定位到第一个ConstantPoolCacheEntry的开始位置 // %rdx*8算出来的是相对于第一个ConstantPoolCacheEntry的字节偏移 0x00007fffe101fd1c: mov 0x10(%rcx,%rdx,8),%ebx // _indices向右移动16位后获取[get bytecode,set bytecode,original constant pool index]中的get bytecode与set bytecode 0x00007fffe101fd20: shr $0x10,%ebx // 获取set bytecode字段的值 0x00007fffe101fd23: and $0xff,%ebx // 0xb2是getstatic指令的Opcode,比较值,如果相等就说明已经连接,跳转到resolved 0x00007fffe101fd29: cmp $0xb2,%ebx 0x00007fffe101fd2f: je 0x00007fffe101fdce // 将getstatic字节码的Opcode存储到%ebx中 0x00007fffe101fd35: mov $0xb2,%ebx // 省略通过调用MacroAssembler::call_VM()函数来执行InterpreterRuntime::resolve_get_put()函数的汇编代码 // ...
调用MacroAssembler::call_VM()函数生成如下代码,通过这些代码来执行InterpreterRuntime::resolve_get_put()函数。MacroAssembler::call_VM()函数的汇编在之前已经详细介绍过,这里不再介绍,直接给出汇编代码,如下:
0x00007fffe101fd3a: callq 0x00007fffe101fd44 0x00007fffe101fd3f: jmpq 0x00007fffe101fdc2 0x00007fffe101fd44: mov %rbx,%rsi 0x00007fffe101fd47: lea 0x8(%rsp),%rax 0x00007fffe101fd4c: mov %r13,-0x38(%rbp) 0x00007fffe101fd50: mov %r15,%rdi 0x00007fffe101fd53: mov %rbp,0x200(%r15) 0x00007fffe101fd5a: mov %rax,0x1f0(%r15) 0x00007fffe101fd61: test $0xf,%esp 0x00007fffe101fd67: je 0x00007fffe101fd7f 0x00007fffe101fd6d: sub $0x8,%rsp 0x00007fffe101fd71: callq 0x00007ffff66b567c 0x00007fffe101fd76: add $0x8,%rsp 0x00007fffe101fd7a: jmpq 0x00007fffe101fd84 0x00007fffe101fd7f: callq 0x00007ffff66b567c 0x00007fffe101fd84: movabs $0x0,%r10 0x00007fffe101fd8e: mov %r10,0x1f0(%r15) 0x00007fffe101fd95: movabs $0x0,%r10 0x00007fffe101fd9f: mov %r10,0x200(%r15) 0x00007fffe101fda6: cmpq $0x0,0x8(%r15) 0x00007fffe101fdae: je 0x00007fffe101fdb9 0x00007fffe101fdb4: jmpq 0x00007fffe1000420 0x00007fffe101fdb9: mov -0x38(%rbp),%r13 0x00007fffe101fdbd: mov -0x30(%rbp),%r14 0x00007fffe101fdc1: retq
如上代码完成的事情很简单,就是调用C++函数编写的InterpreterRuntime::resolve_get_put()函数,此函数会填充常量池缓存中ConstantPoolCacheEntry信息,关于ConstantPoolCache以及ConstantPoolCacheEntry,还有ConstantPoolCacheEntry中各个字段的含义在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》中已经详细介绍过,这里不再介绍。
InterpreterRuntime::resolve_get_put()函数的实现比较多,我们首先看一部分实现,如下:
IRT_ENTRY(void, InterpreterRuntime::resolve_get_put(JavaThread* thread, Bytecodes::Code bytecode)) // resolve field fieldDescriptor info; constantPoolHandle pool(thread, method(thread)->constants()); bool is_put = (bytecode == Bytecodes::_putfield || bytecode == Bytecodes::_putstatic); bool is_static = (bytecode == Bytecodes::_getstatic || bytecode == Bytecodes::_putstatic); { JvmtiHideSingleStepping jhss(thread); int x = get_index_u2_cpcache(thread, bytecode); // 根据线程栈中的bcp来获取常量池缓存索引 LinkResolver::resolve_field_access(info, pool, x ,bytecode, CHECK); // 向info中收集信息 } // check if link resolution caused cpCache to be updated if (already_resolved(thread)){ return; } ... }
调用get_index_u2_cpcache()函数从当前方法对应的栈帧中获取bcp,然后通过bcp来获取字节码指令的操作数,也就是常量池索引,得到常量池索引后调用LinkResolver::resolve_field_access()函数可能会连接类和字段,然后将查询到的字段相关信息存储到fieldDescriptor中。resolve_field_access()函数的实现如下:
void LinkResolver::resolve_field_access( fieldDescriptor& result, constantPoolHandle pool, int index, // 常量池索引 Bytecodes::Code byte, TRAPS ) { Symbol* field = pool->name_ref_at(index); Symbol* sig = pool->signature_ref_at(index); // resolve specified klass 连接特定的类 KlassHandle resolved_klass; resolve_klass(resolved_klass, pool, index, CHECK); KlassHandle current_klass(THREAD, pool->pool_holder()); resolve_field(result, resolved_klass, field, sig, current_klass, byte, true, true, CHECK); }
从pool中查找到的index处的索引项为CONSTANT_NameAndType_info,格式如下:
CONSTANT_NameAndType_info { u1 tag; u2 name_index; // 占用16位 u2 descriptor_index; // 占用16位 }
常量池中的一个CONSTANT_NameAndType_info数据项, 可以看做CONSTANT_NameAndType类型的一个实例 。 从这个数据项的名称可以看出, 它描述了两种信息,第一种信息是名称(Name), 第二种信息是类型(Type) 。这里的名称是指方法的名称或者字段的名称, 而Type是广义上的类型,它其实描述的是字段的描述符或方法的描述符。 也就是说, 如果Name部分是一个字段名称,那么Type部分就是相应字段的描述符; 如果Name部分描述的是一个方法的名称,那么Type部分就是对应的方法的描述符。 也就是说,一个CONSTANT_NameAndType_info就表示了一个方法或一个字段。
调用resolve_klass()连接类,调用resolve_field()连接字段。在resolve_field()函数中有如下实现:
InstanceKlass* tmp = InstanceKlass::cast(resolved_klass()); KlassHandle sel_klass(THREAD, tmp->find_field(field, sig, &fd));
最重要的就是调用InstanceKlass的find_field()函数查找字段,将查找到的相关信息存储到fieldDescriptor类型的fd中。关于字段在InstanceKlass中的存储以及具体的布局在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》中已经详细介绍过,这里不再介绍。
fieldDescriptor类及重要属性的定义如下:
class fieldDescriptor VALUE_OBJ_CLASS_SPEC { private: AccessFlags _access_flags; int _index; // the field index constantPoolHandle _cp; ... }
其中的_access_flags可用来表示字段是否有volatile、final等关键字修饰,_index表示字段是存储在InstanceKlass中相应数组的第几个元组中。_cp表示定义当前字段的类的常量池。
通过调用resolve_klass()和resolve_field()函数后就可拿到这些信息,然后返回到InterpreterRuntime::resolve_get_put()函数继续查看实现逻辑:
TosState state = as_TosState(info.field_type()); Bytecodes::Code put_code = (Bytecodes::Code)0; InstanceKlass* klass = InstanceKlass::cast(info.field_holder()); bool uninitialized_static = ( (bytecode == Bytecodes::_getstatic || bytecode == Bytecodes::_putstatic) && !klass->is_initialized() ); Bytecodes::Code get_code = (Bytecodes::Code)0; if (!uninitialized_static) { get_code = ((is_static) ? Bytecodes::_getstatic : Bytecodes::_getfield); // 1、是putfield或putstatic指令 // 2、是getstatic或getfield指令并且不是获取final变量的值 if (is_put || !info.access_flags().is_final()) { put_code = ((is_static) ? Bytecodes::_putstatic : Bytecodes::_putfield); } } ConstantPoolCacheEntry* cpce = cache_entry(thread); cpce->set_field( get_code, // 设置的是_indices中的b1,当为getstatic或getfield时,则其中存储的是Opcode put_code, // 设置的是_indices中的b2,当为setstatic或setfield时,则其中存储的是Opcode,所以get_code与put_code如果要连接了,其值不为0 info.field_holder(), // 设置的是_f1字段,表示字段的拥有者 info.index(), // field_index,设置的是flags info.offset(), // field_offset,设置的是_f2字段,Offset (in words) of field from start of instanceOop / Klass* state, // field_type,设置的是flags info.access_flags().is_final(), // 设置的是flags info.access_flags().is_volatile(), // 设置的是flags pool->pool_holder() );
通过info中的信息就可以得到字段的各种信息,然后填充ConstantPoolEntry信息,这样下次就不用对字段进行连接了,或者说不用从InstanceKlass中查找字段信息了,可直接从ConstantPoolCacheEntry中找到所有想得到的信息。
上图在《深入剖析Java虚拟机:源码剖析与实例详解(基础卷)》一书中详细介绍过,通过我们解读getstatic字节码的解释执行过程,可以清楚的知道常量池缓存项的作用。对于getstatic来说,开始就会判断_indices中的高8位存储的是否为getstatic的操作码,如果不是,则表示没有连接,所以要调用InterpreterRuntime::resolve_get_put()函数进行连接操作。
在连接完成或已经连接完成时会继续执行如下汇编代码:
// 将ConstantPoolCacheEntry的索引存储么%edx 0x00007fffe101fdc2: movzwl 0x1(%r13),%edx // 将ConstantPoolCache的首地址存储到%rcx 0x00007fffe101fdc7: mov -0x28(%rbp),%rcx // 获取对应的ConstantPoolCacheEntry对应的索引 0x00007fffe101fdcb: shl $0x2,%edx // --resolved -- // 获取[_indices,_f1,_f2,_flags]中的_f2,由于ConstantPoolCache占用16字节,而_indices // 和_f2各占用8字节,所以_f2的偏移为32字节,也就是0x32 // _f2中保存的是字段在java.lang.Class实例中的字节偏移,通过此偏移就可获取此字段存储在 // java.lang.Class实例的值 0x00007fffe101fdce: mov 0x20(%rcx,%rdx,8),%rbx // 获取[_indices,_f1,_f2,_flags]中的_flags 0x00007fffe101fdd3: mov 0x28(%rcx,%rdx,8),%eax // 获取[_indices,_f1,_f2,_flags]中的_f1,_f1保存了字段拥有者, // 也就是java.lang.Class对象 0x00007fffe101fdd7: mov 0x18(%rcx,%rdx,8),%rcx // 从_f1中获取_java_mirror属性的值 0x00007fffe101fddc: mov 0x70(%rcx),%rcx // 将_flags向右移动28位,剩下TosState 0x00007fffe101fde0: shr $0x1c,%eax 0x00007fffe101fde3: and $0xf,%eax // 如果不相等,说明TosState的值不为0,则跳转到notByte 0x00007fffe101fde6: jne 0x00007fffe101fdf6 // btos // btos的编号为0,代码执行到这里时,可能栈顶缓存要求是btos // %rcx中存储的是_java_mirror,%rbx中存储的是_f2,由于静态变量存储在_java_mirror中,所以要获取 // 对应的首地址并压入栈中 0x00007fffe101fdec: movsbl (%rcx,%rbx,1),%eax 0x00007fffe101fdf0: push %rax // 跳转到Done 0x00007fffe101fdf1: jmpq 0x00007fffe101ff0c // -- notByte -- // %eax中存储的是TosState,如果不为atos,则跳转到notObj 0x00007fffe101fdf6: cmp $0x7,%eax 0x00007fffe101fdf9: jne 0x00007fffe101fe90 // atos // %rcx中存储的是_java_mirror,%rbx中存储的是_f2,
// 所以要获取静态变量的首地址并压入栈内 0x00007fffe101fdff: mov (%rcx,%rbx,1),%eax 0x00007fffe101fe02: push %r10 0x00007fffe101fe04: cmp 0x163a8d45(%rip),%r12 # 0x00007ffff73c8b50 0x00007fffe101fe0b: je 0x00007fffe101fe88 0x00007fffe101fe11: mov %rsp,-0x28(%rsp) 0x00007fffe101fe16: sub $0x80,%rsp 0x00007fffe101fe1d: mov %rax,0x78(%rsp) 0x00007fffe101fe22: mov %rcx,0x70(%rsp) 0x00007fffe101fe27: mov %rdx,0x68(%rsp) 0x00007fffe101fe2c: mov %rbx,0x60(%rsp) 0x00007fffe101fe31: mov %rbp,0x50(%rsp) 0x00007fffe101fe36: mov %rsi,0x48(%rsp) 0x00007fffe101fe3b: mov %rdi,0x40(%rsp) 0x00007fffe101fe40: mov %r8,0x38(%rsp) 0x00007fffe101fe45: mov %r9,0x30(%rsp) 0x00007fffe101fe4a: mov %r10,0x28(%rsp) 0x00007fffe101fe4f: mov %r11,0x20(%rsp) 0x00007fffe101fe54: mov %r12,0x18(%rsp) 0x00007fffe101fe59: mov %r13,0x10(%rsp) 0x00007fffe101fe5e: mov %r14,0x8(%rsp) 0x00007fffe101fe63: mov %r15,(%rsp) 0x00007fffe101fe67: movabs $0x7ffff6d4d828,%rdi 0x00007fffe101fe71: movabs $0x7fffe101fe11,%rsi 0x00007fffe101fe7b: mov %rsp,%rdx 0x00007fffe101fe7e: and $0xfffffffffffffff0,%rsp 0x00007fffe101fe82: callq 0x00007ffff6872e3a 0x00007fffe101fe87: hlt 0x00007fffe101fe88: pop %r10 0x00007fffe101fe8a: push %rax 0x00007fffe101fe8b: jmpq 0x00007fffe101ff0c // -- notObj -- 0x00007fffe101fe90: cmp $0x3,%eax // 如果不为itos,则跳转到notInt 0x00007fffe101fe93: jne 0x00007fffe101fea2 // itos 0x00007fffe101fe99: mov (%rcx,%rbx,1),%eax 0x00007fffe101fe9c: push %rax // 跳转到Done 0x00007fffe101fe9d: jmpq 0x00007fffe101ff0c // -- notInt -- // 如果不为ctos,则跳转到notChar 0x00007fffe101fea2: cmp $0x1,%eax 0x00007fffe101fea5: jne 0x00007fffe101feb5 // ctos 0x00007fffe101feab: movzwl (%rcx,%rbx,1),%eax 0x00007fffe101feaf: push %rax // 跳转到Done 0x00007fffe101feb0: jmpq 0x00007fffe101ff0c // -- notChar -- // 如果不为stos,则跳转到notShort 0x00007fffe101feb5: cmp $0x2,%eax 0x00007fffe101feb8: jne 0x00007fffe101fec8 // stos 0x00007fffe101febe: movswl (%rcx,%rbx,1),%eax 0x00007fffe101fec2: push %rax // 跳转到done 0x00007fffe101fec3: jmpq 0x00007fffe101ff0c // -- notShort -- // 如果不为ltos,则跳转到notLong 0x00007fffe101fec8: cmp $0x4,%eax 0x00007fffe101fecb: jne 0x00007fffe101fee2 // ltos 0x00007fffe101fed1: mov (%rcx,%rbx,1),%rax 0x00007fffe101fed5: sub $0x10,%rsp 0x00007fffe101fed9: mov %rax,(%rsp) // 跳转到Done 0x00007fffe101fedd: jmpq 0x00007fffe101ff0c // -- notLong -- // 如果不为ftos,则跳转到notFloat 0x00007fffe101fee2: cmp $0x5,%eax 0x00007fffe101fee5: jne 0x00007fffe101fefe // ftos 0x00007fffe101feeb: vmovss (%rcx,%rbx,1),%xmm0 0x00007fffe101fef0: sub $0x8,%rsp 0x00007fffe101fef4: vmovss %xmm0,(%rsp) // 跳转到Done 0x00007fffe101fef9: jmpq 0x00007fffe101ff0c // -- notFloat -- 0x00007fffe101fefe: vmovsd (%rcx,%rbx,1),%xmm0 0x00007fffe101ff03: sub $0x10,%rsp 0x00007fffe101ff07: vmovsd %xmm0,(%rsp) // -- Done --
如上汇编代码虽然多,但是完成的逻辑却非常简单,就是通过ConstantPoolCacheEntry中存储的信息(所谓的字节码连接完成指的就是对应的常量池缓存项的信息已经完善)完成压栈的逻辑。由于静态字段的值存储在java.lang.Class实例中,所以需要获取到对应的值,然后根据栈顶缓存要求的状态将值压入表达式栈即可。
推荐阅读:
第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)
第20篇-加载与存储指令之ldc与_fast_aldc指令(2)
第21篇-加载与存储指令之iload、_fast_iload等(3)
第22篇-虚拟机字节码之运算指令
第23篇-虚拟机字节码指令之类型转换
如果有问题可直接评论留言或加作者微信mazhimazh
关注公众号,有HotSpot VM源码剖析系列文章!