59 - ASM之JVM Execution Model

Execution Model

什么是Execution Model

Execution Model就是指Stack Frame简化之后的模型。如何“简化”呢?也就是,我们不需要去考虑Stack Frame的技术实现细节,把它想像一个理想的模型就可以了。

针对Execution Model或Stack Frame,我们可以理解成它由local variable和operand stack两个部分组成,或者说理解成它由local variable、operand stack和frame data三个部分组成。换句话说,local variable和operand stack是两个必不可少的部分,而frame data是一个相对来说不那么重要的部分。在一般的描述当中,都是将Stack Frame描述成local variable和operand stack两个部分;但是,如果我们为了知识的完整性,就可以考虑添加上frame data这个部分。

Execution Model

另外,方法的执行与Stack Frame之间有一个非常紧密的关系:

  • 一个方法的调用开始,就对应着Stack Frame的内存空间的分配。
  • 一个方法的执行结束,无论正常结束(return),还是异常退出(throw exception),都表示着相应的Stack Frame内存空间被销毁。
    接下来,我们就通过两个方面来把握Stack Frame的状态:
  • 第一个方面,方法刚进入的时候,任何的instruction都没有执行,那么Stack Frame是一个什么样的状态呢?
  • 第二个方面,在方法开始执行后,这个时候instruction开始执行,每一条instruction的执行,会对Stack Frame的状态产生什么样的影响呢?

方法的初始状态

在方法进入的时候,会生成相应的Stack Frame内存空间。那么,Stack Frame的初始状态是什么样的呢?
在Stack Frame当中,operand stack是空的,而local variables则需要考虑三方面的因素:

  • 当前方法是否为static方法。
    • 如果当前方法是non-static方法,则需要在local variables索引为0的位置存在一个this变量,后续的内容从1开始存放。
    • 如果当前方法是static方法,则不需要存储this,因此后续的内容从0开始存放。
  • 当前方法是否接收参数。方法接收的参数,会按照参数的声明顺序放到local variables当中。
  • 方法参数是否包含long或double类型。如果方法的参数是long或double类型,那么它在local variables当中占用两个位置。
  • 问题:能否在文档中找到依据呢?
    • 回答:能。

The Java Virtual Machine uses local variables to pass parameters on method invocation. On class method invocation, any parameters are passed in consecutive local variables starting from local variable 0. On instance method invocation, local variable 0 is always used to pass a reference to the object on which the instance method is being invoked (this in the Java programming language). Any parameters are subsequently passed in consecutive local variables starting from local variable 1

The operand stack is empty when the frame that contains it is created.

方法的后续变化

方法的后续变化,就是在方法初始状态的基础上,随着instruction的执行而对local variable和operand stack的状态产生影响。

当方法执行时,就是将instruction一条一条的执行:

  • 第一步,获取instruction。每一条instruction都是从instructions内存空间中取出来的。
  • 第二步,执行instruction。对于instruction的执行,就会引起operand stack和local variables的状态变化。
    • 在执行instruction过程中,需要获取相关资源。通过ref可以获取runtime constant pool的“资源”,例如一个字符串的内容,一个指向方法的物理内存地址。

Ignoring exceptions, the inner loop of a Java Virtual Machine interpreter is effectively:

do {
    atomically calculate pc and fetch opcode at pc;
    if (operands) fetch operands;
    execute the action for the opcode;
} while (there is more to do);

需要注意的是,虽然local variable和operand stack是Stack Frame当中两个最重要的结构,两者是处于一个平等的地位上,缺少任何一个都无法正常工作;但是,从使用频率的角度来说,两者还是有很大的差别。先举个生活当中的例子,operand stack类似于“公司”,local variables类似于“临时租的房子”,虽然说“公司”和“临时租的房子”是我们经常待的场所,是两个非常重要的地方,但是我们工作的时间大部是在“公司”进行的,少部分工作时间是在“家”里进行。也就是说,大多数情况下,都是先把数据加载到operand stack上,在operand stack上进行运算,最后可能将数据存储到local variables当中。只有少部分的操作(例如iinc),只需要在local variable上就能完成。所以从使用频率的角度来说,operand stack是进行工作的“主战场”,使用频率就比较高,大多数工作都是在它上面完成;而local variable使用频率就相对较低,它只是提供一个临时的数据存储区域。

查看方法的Stack Frame变化

两个版本

  • 第一个版本,是HelloWorldFrameCore类。它是在《Java ASM系列一:Core API》阶段引入的类,可以用来打印方法的Stack Frame的变化。为了保证与以前内容的一致性,我们保留了这个类的代码逻辑不变动。
  • 第二个版本,是HelloWorldFrameCore02类。它是在《Java ASM系列二:OPCODE》阶段引入的类,在第一个版本的基础上进行了改进:引入了instruction部分,精简了Stack Frame的类型显示。

三个部分

我们在执行HelloWorldFrameCore02类之后,输出结果分成三个部分:

  • 第一部分,是offset,它表示某一条instruction的具体位置或偏移量。
  • 第二部分,是instructions,它表示方法里包含的所有指令信息。
  • 第三部分,是local variable和operand stack中存储的具体数据类型。
    • 格式:{local variable types} | {operand stack types}
    • 第一行的local variable和operand stack表示“方法的初始状态”。
    • 其后每一行instruction
      • 上一行的local variable和operand stack表示该instruction执行之前的状态
      • 与该instruction位于同一行local variable和operand stack表示该instruction执行之后的状态

在上面的输出结果中,我们会看到local variable和operand stack为{} | {}的情况,这是四种特殊情况。

四种情况

在HelloWorldFrameCore02类当中,会间接使用到AnalyzerAdapter类。在AnalyzerAdapter类的代码中,将locals和stack字段的取值设置为null的情况,就会有上面{} | {}的情况。

在AnalyzerAdapter类的代码中,有四个方法会将locals和stack字段设置为null:

  • 在AnalyzerAdapter.visitInsn(int opcode)方法中,当opcode为return或athrow的情况
  • 在AnalyzerAdapter.visitJumpInsn(int opcode, Label label)方法中,当opcode为goto的情况
  • 在AnalyzerAdapter.visitTableSwitchInsn(int min, int max, Label dflt, Label... labels)方法中
  • 在AnalyzerAdapter.visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels)方法中

五个视角

在后续内容中,我们介绍代码示例的时候,一般都会从五个视角来学习:

  • 第一个视角,Java语言的视角,就是sample.HelloWorld里的代码怎么编写。
  • 第二个视角,Instruction的视角,就是javap -c sample.HelloWorld,这里给出的就是标准的opcode内容。
  • 第三个视角,ASM的视角,就是编写ASM代码实现某种功能,这里主要是对visitXxxInsn()方法的调用,与实际的opcode可能相同,也可能有差异。
  • 第四个视角,Frame的视角,就是JVM内存空间的视角,就是local variable和operand stack的变化。
  • 第五个视角,JVM Specification的视角,参考JVM文档,它是怎么说的。

第一个视角,Java语言的视角。假如我们有一个sample.HelloWorld类,代码如下:

package sample;

public class HelloWorld {
    public void test(boolean flag) {
        if (flag) {
            System.out.println("value is true");
        }
        else {
            System.out.println("value is false");
        }
    }
}

第二个视角,Instruction的视角。我们可以通过javap -c sample.HelloWorld命令查看方法包含的instruction内容:

$ javap -c sample.HelloWorld
Compiled from "HelloWorld.java"
public class sample.HelloWorld {
...
  public void test(boolean);
    Code:
       0: iload_1
       1: ifeq          15 (计算之后的值)
       4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #3                  // String value is true
       9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: goto          23 (计算之后的值)
      15: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
      18: ldc           #5                  // String value is false
      20: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      23: return
}

第三个视角,ASM的视角。运行ASMPrint类,可以查看ASM代码,可以查看某一个opcode具体对应于哪一个MethodVisitor.visitXxxInsn()方法:

Label label0 = new Label();
Label label1 = new Label();

methodVisitor.visitCode();
methodVisitor.visitVarInsn(ILOAD, 1);
methodVisitor.visitJumpInsn(IFEQ, label0);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("value is true");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitJumpInsn(GOTO, label1);

methodVisitor.visitLabel(label0);
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitLdcInsn("value is false");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);

methodVisitor.visitLabel(label1);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(2, 2);
methodVisitor.visitEnd();

第四个视角,Frame的视角。我们可以通过运行HelloWorldFrameCore02类来查看方法对应的Stack Frame的变化:

test:(Z)V
                               // {this, int} | {}
0000: iload_1                  // {this, int} | {int}
0001: ifeq            14(真实值)// {this, int} | {}
0004: getstatic       #2       // {this, int} | {PrintStream}
0007: ldc             #3       // {this, int} | {PrintStream, String}
0009: invokevirtual   #4       // {this, int} | {}
0012: goto            11(真实值)// {} | {}
                               // {this, int} | {}
0015: getstatic       #2       // {this, int} | {PrintStream}
0018: ldc             #5       // {this, int} | {PrintStream, String}
0020: invokevirtual   #4       // {this, int} | {}
                               // {this, int} | {}
0023: return                   // {} | {}

第五个视角,JVM Specification的视角。
Format

mnemonic
operand1
operand2
...

Operand Stack

..., value1, value2 →

..., value3

小结

  • Execution Model就是对Stack Frame进行简单、理想化之后的模型;对于Stack Frame来说,我们要关注方法的初始状态和方法的后续变化。
  • 通过运行HelloWorldFrameCore02类可以查看具体方法的Stack Frame变化。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容