这周例会上,在进行 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 运行时内存规划中有一块区域叫做虚拟机栈,被划分为线程私有的空间,线程中方法的运行则对应该区域栈帧的入栈,出栈,一个方法对应一个栈帧。
从上图中可以看到,栈帧单元分为四部分:局部变量表,操作数栈,动态连接,返回地址。
存放方法运行所需的局部变量:方法参数,内部定义的局部变量。
最小存放单元为 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 复制代码
关于局部变量表还有两个有趣的点可以探索,本文先不涉及这块内容。
用于存放操作指令执行所需要的操作数,延续上一个简单的例子
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 复制代码
简单解释一下几个指令
上述例子代码的执行过程大致如下:
可以看出,操作数栈会将运算结果即时刷新到局部变量表对应的变量。
讲到这里,相信大家对于 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 局部变量定义在循环外部