转载

jvm探索之路-基于栈的方法执行机制

这周例会上,在进行 code review 时,有同事对其中一个方法变量的定义位置提出疑问

// 为了方便,对代码进行了简化
		public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            int a = 1;
            int b = 2;
            System.out.println(a + b);
        }
    }
复制代码

他认为 for 循环中 a,b 提到方法外部定义会好一些,但是具体缘由也没说清楚,有些同事还扯上运行性能,垃圾回收。

但站在我的角度,这两者的区别,除了程序层面变量的作用域变了,在方法运行层面是没有明显差异的,这边也正好回顾一下 jvm 方法运行相关的机制。

虚拟机栈

jvm 运行时内存规划中有一块区域叫做虚拟机栈,被划分为线程私有的空间,线程中方法的运行则对应该区域栈帧的入栈,出栈,一个方法对应一个栈帧。

jvm探索之路-基于栈的方法执行机制

从上图中可以看到,栈帧单元分为四部分:局部变量表,操作数栈,动态连接,返回地址。

局部变量表

存放方法运行所需的局部变量:方法参数,内部定义的局部变量。

最小存放单元为 Slot(变量槽) ,空间大小为 4 个字节,因此像boolean,byte,char,short,int,float,reference 只需要占用一个变量槽,而 long,double 则需要占用两个变量槽。

因此局部变量表的大小在编译阶段就能确定,与程序运行与否没有太大关系。

举个简单的例子

public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = a + b;
    }
复制代码

对应的局部变量表为:这边重点关注一下 Slot 的值,它是根据变量定义顺序分配的索引值(例如 args 是方法参数,认为是第一个定义的局部变量,因此 args 的 slot = 0)

LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0       9     0  args   [Ljava/lang/String;
        2       7     1     a   I
        4       5     2     b   I
        8       1     3     c   I
复制代码

关于局部变量表还有两个有趣的点可以探索,本文先不涉及这块内容。

  • 栈帧之间存储空间复用(操作数栈与局部变量表,用于方法调用参数的传递)
  • 局部变量表 Slot 的复用(可能会影响对象的垃圾回收)

操作数栈

用于存放操作指令执行所需要的操作数,延续上一个简单的例子

public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = a + b;
    }
复制代码

对应的方法字节码指令集:

0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
复制代码

简单解释一下几个指令

  • iconst 将对应常量放入操作数栈,例 iconst_1 是将 1 放入操作数栈
  • istore 将操作数栈顶元素的值写入局部变量表指定槽位,例如 istore_1 是将操作数栈顶元素的值写入局部变量表 slot = 1 的变量
  • iload 将指定槽位的局部变量值放入操作数栈顶,例 iload_1 是将局部变量表 slot = 1 的变量值放入操作数栈顶
  • iadd 弹出操作数栈顶的两个元素,进行 + 操作,将结果再压回操作数栈

上述例子代码的执行过程大致如下:

  • 常量 1 入栈,将栈顶元素赋值给局部变量表的变量 a =》 iconst_1,istore_1
  • 常量 2 入栈,将栈顶元素赋值给局部变量表的变量 b =》 iconst_2,istore_2
  • 将局部变量表中变量 a ,b 的值入栈 =》 iload_1,iload_2
  • 弹出栈顶的前两个元素,进行 + 的操作,将结果入栈 =》iadd
  • 将栈顶元素,也就是计算结果赋值给局部变量表的变量 c =》 istore_3

可以看出,操作数栈会将运算结果即时刷新到局部变量表对应的变量。

这边对于动态连接,还有返回地址暂时不作讨论,不是本文的重点。

字节码分析

讲到这里,相信大家对于 jvm 方法的解释执行机制有了一定的认识,下面我们回到开头的例子,探索一下这个局部变量定义位置的问题。

使用 javap 命令看一下程序对应的字节码文件内容,首先关注局部变量表,该表用来存放方法定义的局部变量,按照定义的顺序,可以看到 a,b 被分配在 2,3 槽位(slot),操作数栈运算需要用到这个 slot 索引(istore_2,iload_2 指令后面的数字就是指这个 slot 索引)

LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10      11     2     a   I
           12       9     3     b   I
            2      25     1     i   I
            0      28     0  args   [Ljava/lang/String;

复制代码

如果我们把局部变量定义在 for 循环外面

// 为了方便,对代码进行了简化
		public static void main(String[] args) {
        int a;
        int b;
        for (int i = 0; i < 100; i++) {
						a = 1;
          	b = 2;
            System.out.println(a + b);
        }
    }
复制代码

对应字节码的局部变量表,可以看到 a,b 除了变量槽 slot 的索引变了(因为定义顺序发生变化),其它都相同,在方法执行层面,可以认为它们是一致的。

LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10      17     1     a   I
           12      15     2     b   I
            2      25     3     i   I
            0      28     0  args   [Ljava/lang/String;

复制代码

现在我们来看一下方法对应的字节码指令,可以看到内容几乎是一样的,除了 istore,iload 指令存在差异,是因为 a,b 在变量槽中的索引发生了变化,在方法执行层面,也可以认为它们是一致的。

// 局部变量定义在方法外部
 Code:
      stack=3, locals=4, args_size=1
         0: iconst_0
         1: istore_3
         2: iload_3
         3: bipush        100
         5: if_icmpge     27
         8: iconst_1
         9: istore_1
        10: iconst_2
        11: istore_2
        12: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: iload_1
        16: iload_2
        17: iadd
        18: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        21: iinc          3, 1
        24: goto          2
        27: return
        
 // 局部变量定义在方法内部
    Code:
      stack=3, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: bipush        100
         5: if_icmpge     27
         8: iconst_1
         9: istore_2
        10: iconst_2
        11: istore_3
        12: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: iload_2
        16: iload_3
        17: iadd
        18: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        21: iinc          1, 1
        24: goto          2
        27: return


复制代码

结论

从实验现象来看,局部变量定义在循环内部 vs 局部变量定义在循环外部

  • 二者 字节码中方法的局部变量表是一致的
  • 二者 字节码中方法对应的执行指令是一致的

从这两个纬度基本就可以确认无疑,二者在程序运行层面上是没有明显差异的。

原文  https://juejin.im/post/5e7b4a585188255e354fed26
正文到此结束
Loading...