JVM StackMapTable 屬性的作用及理解

 

      Java 6版本之後JVM在class文件中引入了棧圖(StackMapTable)屬性。作用是爲了提高JVM在類型檢查的驗證過程的效率,以下簡稱StackMapTable爲棧圖。 棧圖結構位於Code屬性(指Classfile的Code屬性)的屬性表( attributes table)結構中。在字節碼的Code屬性中最多包含一個StackMapTable屬性。在Java 7版本之後把棧圖作爲字節碼文件中的強制部分。 本來程序員是不需要關心JVM中的JIT編譯器的細節,也不用知道編譯原理或者數據流、控制流的細節。但棧圖強制了,如果要生成bytecode,必須準確知道每個字節碼指令對應的局部變量和操作數棧的類型。這是因爲Java7在編譯的時期做了一些驗證期間要做的事情,那就是類型檢查,也就是棧圖包含的內容。

     想想都比較抓狂,但是JVM做的這一點點性能優化對整體性能提升也沒起到什麼卵用。Java的驗證在類加載的時候只會運行一次,而佔據了大部分時間的操作是IO的消耗,而不是驗證過程。即使現在有了棧圖,驗證過程依然會執行,棧圖的存在只是節省了一部分的驗證時間。並且JVM的設計者還必須兼容沒有棧圖的驗證的實現,因爲Java7以前版本是沒有強制棧圖這個概念的,然而Java8依然延續了棧圖的字節碼結構。

   jvm8的規範中(https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.4),棧圖的標準結構如下:

StackMapTable_attribute {
    u2              attribute_name_index;
    u4              attribute_length;
    u2              number_of_entries;
    stack_map_frame entries[number_of_entries];
}

  

  1>attribute_name_index:對應的是常量池表的一個有效索引。也即CONSTANT_Utf8_info結構中表示“StackMapTable”的索引。

  2>attribute_length:標識當前屬性的長度(排除前六個字節)

  3> number_of_entries:表示entries表的成員數量。entries表中的所有成員都是一個stack_map_frame結構。

 4> entries[]:entries表中的每一項都表示本方法的一個stack map frame,並且表中每一項都是有序的。 


  下面來結合一個例子看一下棧圖的結構。

   Java代碼如下:

package bytecode;
 
/**
 * Created by yunshen.ljy on 2015/6/16.
 */
public class Coffee {
 
    int bean;
 
    public void getBean(int var) {
        if (var > 0) {
            this.bean = var;
        } else {
            throw new IllegalArgumentException();
        }
    }
 
}

 

     使用Verbose來查看Class文件結構,如下:重點看StackMapTable,棧圖包含了兩個entry

public class com.lijingyao.bytecode.Coffee
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#18         // java/lang/Object."<init>":()V
   #2 = Fieldref           #5.#19         // com/lijingyao/bytecode/Coffee.bean:I
   #3 = Class              #20            // java/lang/IllegalArgumentException
   #4 = Methodref          #3.#18         // java/lang/IllegalArgumentException."<init>":()V
   #5 = Class              #21            // com/lijingyao/bytecode/Coffee
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               bean
   #8 = Utf8               I
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               getBean
  #14 = Utf8               (I)V
  #15 = Utf8               StackMapTable
  #16 = Utf8               SourceFile
  #17 = Utf8               Coffee.java
  #18 = NameAndType        #9:#10         // "<init>":()V
  #19 = NameAndType        #7:#8          // bean:I
  #20 = Utf8               java/lang/IllegalArgumentException
  #21 = Utf8               com/lijingyao/bytecode/Coffee
  #22 = Utf8               java/lang/Object
{
  int bean;
    descriptor: I
    flags:


  public com.lijingyao.bytecode.Coffee();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0


  public void getBean(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_1
         1: ifle          12
         4: aload_0
         5: iload_1
         6: putfield      #2                  // Field bean:I
         9: goto          20
        12: new           #3                  // class java/lang/IllegalArgumentException
        15: dup
        16: invokespecial #4                  // Method java/lang/IllegalArgumentException."<init>":()V
        19: athrow
        20: return
      LineNumberTable:
        line 10: 0
        line 11: 4
        line 13: 12
        line 15: 20
      StackMapTable: number_of_entries = 2
        frame_type = 12 /* same */
        frame_type = 7 /* same */
}

 

 

       Classfile的常量池表結構中可以看到#15 的utf8結構屬性標示了StackMapTable結構。最後幾行中可以看到getBean(int) 方法
具體的
StackMapTable結構,這個就是棧圖。從上文可知StackMapTable包含了attribute_name_index,attribute_length,number_of_entries以及entries結構。其中number_of_entries代表了stack map frame的個數,也即entries個數,本例中可以看到有兩個“frame_type”即=2。entries中的兩項分別是  frame_type = 12 /* same */ 和frame_type = 7 /* same */。每一個entry元素都代表了一個方法的StackMapFrame。其包含了某字節碼的偏移量(表示該幀對應的字節碼位置)以及此偏移量處的局部變量表( local variables)、操作數棧(operand stack entry)所需的驗證類型(ps:關於局部變量表和操作數棧可以參考http://blog.csdn.net/lijingyao8206/article/details/46562933 的介紹)。每個方法的第一個StackMapFrame是隱式的(entries[0]),並且是通過類型檢查器的方法描述計算出來。這裏我們看到的frame_type = 12 /* same */ 其實是方法的第二個StackMapFrame,只不過是顯示的StackMapFrame。entries表中的每個stack map frame都依賴於前一個元素,每一項都是使用偏移量的增量來表示。所以entry的順序是很重要的。

     這裏先補充一點字節碼指令和參數概念,字節碼的指令,是由一個字節長度的助記符表示的操作碼(Opcode)以及其隨後的需要操作的若干參數構成。有的指令並不一定需要參數。但這裏注意不要混淆一個概念,這裏的參數和操作數(oprends)不是同一個概念。這裏的arguments(參數)是靜態的值,編譯期就存儲在編譯後的字節碼中,而Oprends(操作數)的值第一節介紹的操作數棧中運行期才知道值的數據結構。不知道講清楚沒有,但發現很多譯文以及文章都會混淆指令集的“參數”和操作數棧的“操作數”。其實參數是以一個字節爲單位的有符號整型,用於指向跳轉目標地址,如果是超過一個字節,就以兩個參數存儲,兩個參數還是依照高位在前的方式存儲。 如:目標指令地址 = goto指令地址 + ( 參數1 << 8 | 參數2 )

     因爲棧圖中的stack map frame結構中的entries是使用偏移量的增量來標識的,可以根據offset_delta+1 公式來根據每個顯示幀算出下一個顯示幀的偏移量。即示例方法getBean的偏移量要這樣計算,在本例中第一個顯示的entries項:frame_type =12 ,這裏12是這一個frame的字節碼偏移量(offset_delta)。而下一個元素的偏移量是前一個元素的offset_delta+1+當前frame的偏移量。所以我們看到

1: ifle          12

這一行中,ifle 字節碼指令的參數是12,所以entries中第一個StackMapFrame元素的字節碼偏移量是offset_delta=12 ,同理

 9: goto          20

 這一行 goto 字節碼指令的參數是20 ,其實是goto 12+1+7,也即goto 指令的字節碼偏移量是20。所以StackMapTable通過記錄偏移量來保證字節序,並且不會重複記錄。可以發現,StackMapTable不過是給JVM類型檢查的驗證階段增加了一些對於字節碼指令偏移量的信息,通過增量的計算方式,簡化了對於方法中所有字節碼偏移量的的檢查。

    本例中的StackMapFrameframe_type /* same */項表示當前幀和前一幀有相同的局部變量,並且當前操作數棧爲空。對於當前局部變量表和操作數棧的數據流可以參考之前的一篇例子。http://blog.csdn.net/lijingyao8206/article/details/46582541。本文主要結合JVM 8規範,如有錯誤請指正。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章