JVM StackMapTable 属性的作用及理解
发布日期:2021-07-01 05:14:52 浏览次数:2 分类:技术文章

本文共 5395 字,大约阅读时间需要 17 分钟。

 

      在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的规范中(),栈图的标准结构如下:

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_SUPERConstant pool:   #1 = Methodref          #6.#18         // java/lang/Object."
":()V   #2 = Fieldref           #5.#19         // com/lijingyao/bytecode/Coffee.bean:I   #3 = Class              #20            // java/lang/IllegalArgumentException   #4 = Methodref          #3.#18         // java/lang/IllegalArgumentException."
":()V   #5 = Class              #21            // com/lijingyao/bytecode/Coffee   #6 = Class              #22            // java/lang/Object   #7 = Utf8               bean   #8 = Utf8               I   #9 = Utf8               
  #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         // "
":()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."
":()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."
":()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:关于局部变量表和操作数栈可以参考 的介绍)。每个方法的第一个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类型检查的验证阶段增加了一些对于字节码指令偏移量的信息,通过增量的计算方式,简化了对于方法中所有字节码偏移量的的检查。

    本例中的StackMapFrame的frame_type /* same */项表示当前帧和前一帧有相同的局部变量,并且当前操作数栈为空。对于当前局部变量表和操作数栈的数据流可以参考之前的一篇例子。。本文主要结合JVM 8规范,如有错误请指正。

 

 

转载地址:https://nanyao.blog.csdn.net/article/details/46715405 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:ASM(三) 利用Method组件动态生成方法的字节码
下一篇:Java 并发包之线程池和原子计数

发表评论

最新留言

表示我来过!
[***.240.166.169]2024年04月14日 06时07分11秒

关于作者

    喝酒易醉,品茶养心,人生如梦,品茶悟道,何以解忧?唯有杜康!
-- 愿君每日到此一游!

推荐文章