第23篇-虛擬機對象操作指令之getstatic

  • 2021 年 9 月 15 日
  • 筆記

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篇-虛擬機位元組碼之運算指令

第22篇-虛擬機位元組碼指令之類型轉換

如果有問題可直接評論留言或加作者微信mazhimazh

關注公眾號,有HotSpot VM源碼剖析系列文章!