虚拟机字节码执行引擎1--虚拟机栈与栈帧

虚拟机对方法执行的支持是通过虚拟机栈来实现的。具体地:栈帧入栈代表着方法开始执行,方法执行完成时栈帧出栈。
如下图:main方法开始执行的时候,其对应的栈帧1入栈。在main中调用method1,于是对应的栈帧2入栈。随后,method1调用方法2,对应的栈帧3入栈,方法2执行结束后,栈帧3出栈,此时栈顶是method1的栈帧2。接下来method3对应的栈帧4入栈...


操作数栈

1.运行时栈帧结构

每个栈帧包含了局部变量表、操作数栈、动态链接、方法返回地址和一些额外的信息。
每个栈帧中,局部变量表的大小和操作数栈的最大深度都可以在编译的时候计算出来,存储在class文件中,运行时转化成方法区的数据。

1.1 局部变量表

局部变量表用于存储方法的参数和方法内定义的局部变量。这些变量被存放在变量槽(variable slot)中,每个变量曹都能存放一个bool、byte、short、char、int、float、reference、returnAddress类型的数据。其中reference表示一个实例对象的引用,通过它能够找到该对象在堆内存中的起始地址或索引,同时还能找到对象所属数据类型在方法区存储的类型信息(比如常用的xxx.getClass(),虽然它是找的堆中的Class对象)。returnAddress类型较少见。

如果执行的是实例方法而非static修饰的静态方法,方法的参数实际上还包含了方法所属对象的引用,该引用被存储在局部变量表第0位索引的变量槽。

由于操作数栈是线程私有的,因此对局部变量表内数据的修改不会引发线程安全问题。(当然,作为程序员也没啥机会去修改局部变量表数据)。

在垃圾回收中,局部变量表可以作为一个GC Root。原因很好理解,局部变量表中所引用的对象,在当前时刻都还是有用的,不应当作为垃圾被回收。

1.2 操作数栈

JVM的解释执行引擎是基于栈的执行引擎,这里的栈就是指操作数栈。
对于每一条存数的指令,如iload、ipush...,虚拟机都会将对应的操作数压入操作数栈顶;在取数指令(如 istore...)时,从栈顶弹出对应数量的操作数。当然也有既要取数又要存数的指令,如(iadd,它从栈顶弹出两个操作数,完成假发操作后将结果压入到操作数栈)。

1.3 动态连接

在虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。jvm中方法调用指令将常量池中指向方法的符号引用作为参数,其目的是支持方法调用过程中的动态连接。

所谓方法调用,并不是说方法中的代码被执行,而是确定要调用哪一个方法(即方法版本)。

动态连接,是相对于静态解析而言的。在类加载或第一次使用这些符号引用的时候,会将部分符号引用转成直接引用,这种方式被称为静态解析;比如静态方法和私有方法,前者与类型直接关联,后者在外部不可访问,因此不会有其他版本的方法。还有一部分符号引用要在每次运行期间转化成直接引用,这种方式就是动态连接,比如方法重载与重写。

1.4 方法返回地址

一个方法开始执行后,有两种退出的方式。一种是执行引擎遇到了代表方法返回的字节码指令正常退出,另一种是执行过程中遇到了异常。
无论是否正常退出,都需要返回方法被调用的位置,继续执行后面的程序。正常退出时,主调方法的PC计数器就可以作为返回地址,可以将它存放在栈帧中。但方法异常退出时,返回地址要通过异常处理器表来确定,战阵中通常不会保存该信息。

2. 举个例子

2.1 首先通过一个例子来理解一下基于栈的解释器执行过程。在这里主要关注局部变量表与操作数栈

    public static void main(String[] args) {
        int a = 100;
        int b = a++;
        int c = ++a;
        // System.out.println("b: " + b + "\n" + "c: " + c);
    }

其字节码指令如下idea安装Jclasslib即可查看

 0 bipush 100
 2 istore_1
 3 iload_1
 4 iinc 1 by 1
 7 istore_2
 8 iinc 1 by 1
11 iload_1
12 istore_3
13 return
  • 首先是两条表示int a = 100的字节码
    执行偏移地址0处的指令,bipush 100。 将单字节整型常量值100压入操作数栈
    此时局部变量表和操作数栈如下图


    地址0处

    执行istore_1,将操作数栈顶的100弹出并存放到局部变量表1的位置。


    地址2处执行
  • 然后是int b = a++表示的字节码
    iload_1,将局部变量表1处的值复制到操作数栈顶


    iload_1执行之后

    iinc 1 by 1:这条指令的作用是将局部变量表1处的值自增1.执行结束后,如下图


    iinc 1 by 1执行之后

    istore_2。 将栈顶元素弹出赋给局部变量表2处的值。
    istore_2执行完毕
  • 接下来进入++a所表示的字节码
    iinc 1 by 1,结束后ru


    iinc 1 by 1 (地址8处指令)执行结束后

iloa_1:将1处的102压入栈顶


iload_1(地址11处)执行完毕

istore_3执行完毕后,102出栈,复制到局部变量表3处。


istore_3执行完毕后

本方法结束后,局部变量a, b , c的值就存放在局部变量表内。

2.2然后通过一个更复杂一点的例子,来看看

这段代码与上面的一段看起来很像,但是,结果却大不相同。

public class AddA {
    public static void main(String[] args) {
        int a = 100;
        int b = useBeforeIncrement(a);
        int c = IncreseBeforeUse(a);
        System.out.println("a: "+ a + "\nb: " + b + "\nc: " + c);
    }

    private static int useBeforeIncrement(int a) {
        a = a++;
        return a;
    }
    private static int IncreseBeforeUse(int a) {
        a = ++a;
        return a;
    }
}

------结果------
a: 100
b: 100
c: 101

分析一下字节码指令,便可以看出究竟哪里不同

  • main方法对应的字节码指令:
 0 bipush 100
 2 istore_1
 3 iload_1
 4 invokestatic #2 <AddA.useBeforeIncrement>
 7 istore_2
 8 iload_1
 9 invokestatic #3 <AddA.IncreseBeforeUse>
12 istore_3
// 后面都是关于print方法的字节码,忽略掉
  • useBeforeIncrement(int a)对应的字节码指令
0 iload_0
1 iinc 0 by 1
4 istore_0
5 iload_0
6 ireturn
  • IncreseBeforeUse(int a)对应的字节码指令
0 iinc 0 by 1
3 iload_0
4 istore_0
5 iload_0
6 ireturn

首先main方法对应的栈帧压入虚拟机栈。向局部变量表存入100,并复制到操作数栈顶


main方法栈帧

然后调用静态方法AddA类的useBeforeIncrement, 同时取出操作数栈顶的100作为参数传递到AddA.useBeforeIncrement对应栈帧的局部变量表中。


useBeforeIncrement iload_1开始之前

在useBeforeIncrement中,先将0处的100压入操作数栈,然后对局部变量表0处的值自增iinc 0 by 1。接下来执行istore_1,将栈顶的100存回到局部变量表。我们发现,局部变量表内的数据没有发生任何变化,也好理解,因为a = a++是两条指令,a++讲局部变量表0处位置+1,然后赋值又是从栈顶弹出元素并复制到0处,因此a的值不会发生任何变化。


useBeforeIncrement istore4执行之后

最后返回a,即将0处的值压入栈顶,然后弹出,返回给调用当前方法(useBeforeInc)的方法(main)。

胡一刀main方法,将useBeforeIncrement的返回值存入局部变量表1处(赋值给变量b), 而a仍然是原来的100。
然后iload_1将100压入栈顶,作为IncreseBeforeUse的实际参数。


给b赋值之后main方法栈帧

接下来执行AddA.IncreseBeforeUse(int a)方法,与useBeforeIncrement原理相似,区别只是在于++a的字节码指令不同。
执行结束后,main方法栈桢如图:


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