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)
如果有問題可直接評論留言或加作者微信mazhimazh
關注公眾號,有HotSpot VM原始碼剖析系列文章!