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實例中,所以需要獲取到對應的值,然后根據棧頂緩存要求的狀態將值壓入表達式棧即可。
推薦閱讀:
第2篇-JVM虛擬機這樣來調用Java主類的main()方法
第13篇-通過InterpreterCodelet存儲機器指令片段
第20篇-加載與存儲指令之ldc與_fast_aldc指令(2)
第21篇-加載與存儲指令之iload、_fast_iload等(3)
如果有問題可直接評論留言或加作者微信mazhimazh
關注公眾號,有HotSpot VM源碼剖析系列文章!