前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM的StackMapTable的前世今生

JVM的StackMapTable的前世今生

作者头像
JavaEdge
发布2020-05-26 17:02:49
1.8K0
发布2020-05-26 17:02:49
举报
文章被收录于专栏:JavaEdgeJavaEdge

在Java 6版本之后JVM在class文件中引入了栈图(StackMapTable)。

  • 作用 为了提高JVM在类型检查的验证过程的效率

栈图结构位于Code属性(指Classfile的Code属性)的属性表( attributes table)结构中。在字节码的Code属性中最多包含一个StackMapTable属性。 在Java 7版本之后把栈图作为字节码文件中的强制部分。 本来程序员是不需要关心JVM中的JIT编译器的细节,也不用知道编译原理或者数据流、控制流的细节。但栈图强制了,如果要生成bytecode,必须准确知道每个字节码指令对应的局部变量和操作数栈的类型。这是因为Java7在编译的时期做了一些验证期间要做的事情,那就是类型检查,也就是栈图包含的内容。

但是Java的验证在类加载的时候只会运行一次,而占据了大部分时间的操作是IO的消耗,而不是验证过程。即使现在有了栈图,验证过程依然会执行,栈图的存在只是节省了一部分的验证时间。并且JVM的设计者还必须兼容没有栈图的验证的实现,因为Java7以前版本是没有强制栈图这个概念的,然而Java8依然延续了栈图的字节码结构。

mally, you do not need to know much about the stackmap Table when using ASM. ASM can automatically generate this table for you unless you initialize the classwriter with COMPUTE_FRAME flags.For detailed table definition, you can consult the JVM specification (After Java7? ). The only place I know for the stackmap table is the bytecode verification when a class loader is about to load a bytecode, or you want to see the legal of bytecode you generated. In this case, verifier takes the entries in the table to make the correctness of bytecodes (e.g., type consistence, jump targets and so on). Check this link http://stackoverflow.com/questions/25109942/is-there-a-better-explanation-of-stack-map-frames if you want to detail explanation.

下面来结合一个例子看一下栈图的结构。

Java代码如下:

代码语言:javascript
复制
package bytecode;

public class Coffee {
 
    int bean;
 
    public void getBean(int var) {
        if (var > 0) {
            this.bean = var;
        } else {
            throw new IllegalArgumentException();
        }
    }
 
}

使用Verbose来查看Class文件结构,如下:重点看StackMapTable,栈图包含了两个entry。

代码语言:javascript
复制
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类型检查的验证阶段增加了一些对于字节码指令偏移量的信息,通过增量的计算方式,简化了对于方法中所有字节码偏移量的的检查。

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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-04-01 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档