Java中静态变量、局部变量自增自减的差异
上一篇博客中我们理解了Java中局部变量的自增自减在字节码层面的实现,那静态变量的自增自减是否和局部变量的相同呢?我们还是先从下面这道题入手分析:
public class Test2 {
static int j;
public static void main(String[] args) {
for(int i=0;i<10;i++){
j=(j++);
}
System.out.println(j);
}
}
相信你已经有了答案(没有的话运行一下不就有了),输出的是0。那不是和上篇博客局部变量的自增那道题目一样?是的,那说完了。
那肯定不能够啊,这样分析也太不严谨了。我们还是javap反编译一下。
Classfile /D:/IDEA/WorkSpace/out/production/JavaBasic/self_increment/Test2.class
Last modified 2020年5月30日; size 624 bytes
SHA-256 checksum 00a338b23c4830ebdfbae6401a9c3d5a9a8859302f37f700a5eba793e39ce40d
Compiled from "Test2.java"
public class self_increment.Test2
minor version: 0
major version: 55
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #5 // self_increment/Test2
super_class: #6 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#25 // self_increment/Test2.j:I
#3 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #28.#29 // java/io/PrintStream.println:(I)V
#5 = Class #30 // self_increment/Test2
#6 = Class #31 // java/lang/Object
#7 = Utf8 j
#8 = Utf8 I
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lself_increment/Test2;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 i
#19 = Utf8 args
#20 = Utf8 [Ljava/lang/String;
#21 = Utf8 StackMapTable
#22 = Utf8 SourceFile
#23 = Utf8 Test2.java
#24 = NameAndType #9:#10 // "<init>":()V
#25 = NameAndType #7:#8 // j:I
#26 = Class #32 // java/lang/System
#27 = NameAndType #33:#34 // out:Ljava/io/PrintStream;
#28 = Class #35 // java/io/PrintStream
#29 = NameAndType #36:#37 // println:(I)V
#30 = Utf8 self_increment/Test2
#31 = Utf8 java/lang/Object
#32 = Utf8 java/lang/System
#33 = Utf8 out
#34 = Utf8 Ljava/io/PrintStream;
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (I)V
{
static int j;
descriptor: I
flags: (0x0008) ACC_STATIC
public self_increment.Test2();
descriptor: ()V
flags: (0x0001) 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 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lself_increment/Test2;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 26
8: getstatic #2 // Field j:I
11: dup
12: iconst_1
13: iadd
14: putstatic #2 // Field j:I
17: putstatic #2 // Field j:I
20: iinc 1, 1
23: goto 2
26: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
29: getstatic #2 // Field j:I
32: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
35: return
LineNumberTable:
line 7: 0
line 8: 8
line 7: 20
line 10: 26
line 11: 35
LocalVariableTable:
Start Length Slot Name Signature
2 24 1 i I
0 36 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 2
locals = [ int ]
frame_type = 250 /* chop */
offset_delta = 23
}
SourceFile: "Test2.java"
分析
我们还是只关心我们感兴趣的部分
LocalVariableTable:
Start Length Slot Name Signature
2 24 1 i I
0 36 0 args [Ljava/lang/String;
可以看到,局部变量表中只有i和args两个变量了,j不是局部变量,它不在里面特别合理。那它跑去哪了呢?我们知道,static修饰的变量就是静态变量,它是属于类的,只有一份,只被加载一次。前面有提到,JVM内存结构中有一块叫方法区,方法区用于存储已被虚拟机加载的类信息、 常量、 静态变量等。
所以j这个静态变量就是存储在方法区中的,方法区中有一块区域叫常量池,所以我们到常量池中找一下。
常量池内容太多,我把内容挑出来以下面这幅图呈现:
经过上图的分析,常量池中就#2对应的就是我们的静态变量j。
接下来,我们就应该来看操作数栈中虚拟机指令的执行情况:
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 26
8: getstatic #2 // Field j:I
11: dup
12: iconst_1
13: iadd
14: putstatic #2 // Field j:I
17: putstatic #2 // Field j:I
20: iinc 1, 1
23: goto 2
26: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
29: getstatic #2 // Field j:I
32: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
35: return
一眼看过去,就dup和putstatic这两条指令还不认识,其他指令都在上篇博客有详细说明。putstatic就很好理解了,因为getstatic是获取指定类的静态字段,并压入栈顶,那putstatic自然就是将栈顶的值放回去,从哪里拿自然就放回哪里去。那dup这条指令是什么意思呢?这条指令就是复制栈顶数值并将复制值压入栈顶。了解完这些,我们还是逐行分析指令。
- 0: iconst_0,将值为0的int类型压入栈顶。
- 1: istore_1,将栈顶值出栈并存入1号槽位,即i=0。
- 2: iload_1,将1号槽位上的值压入栈顶。
- 3: bipush 10,将10压入栈顶。
- 5: if_icmpge 26,比较栈顶两个int类型的值,当前者(先入栈的i)大于等于后者(后入栈的10),跳转到26行。显然这是for循环条件不满足后跳出,条件满足时则继续往下走。
- 8: getstatic #2,根据#2在常量池中找,刚刚分析过了,就是静态变量j。经过类的加载、链接、初始化后,其值为0,这一步将0压入栈顶。
- 11: dup,复制栈顶数值0,并将复制值0压入栈顶。这一步相当于栈顶部存在两个0。
- 12: iconst_1,将值为1的int类型压入栈顶。
- 13: iadd,将栈顶两int类型数值相加并将结果压入栈顶。
- 14: putstatic #2,将栈顶数值1存入#2对应的静态变量j中。
- 17: putstatic #2,将栈顶数值0(被复制的0)存入#2对应的静态变量j中。
- 20: iinc 1, 1,将i++。
- 23: goto 2,跳转到第二行,即又一轮for循环。
- 26: getstatic #3,根据#3在常量池中找,javap工具给出的注释内容即#3在常量池对应的是System.out对象,它的类型是PrintStream。
- 29: getstatic #2 ,获取j的值并压入栈顶。
- 32: invokevirtual #4 ,根据#4在常量池中找,它对应的是println:(I)V,参数是I,即Integer。
- 35: return,main方法结束。
经过分析,我们发现dup指令会将j的值复制一份,复制值与1作相加运算并赋值给j,j原先的值0又赋值给j,这样每一次for循环j的值都为0,最终输出j,即为0。正是由于dup和两次putstatic,使得每次静态变量j都被重置为0。
我们还可以看到,局部变量i的自增是指令iinc完成,且是在局部变量表中完成,无需压入操作数栈;而静态变量j的自增是由指令getstatic、iconst_1、iadd、putstatic完成,是需要在操作数栈中完成的。
到这,可能还是有些疑惑,JVM为什么要这么做呢?
我们把代码改一下
public class Test2 {
static int j;
public static void main(String[] args) {
for(int i=0;i<10;i++){
j++;
}
System.out.println(j);
}
反编译,这次我只拿重点的那一块
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 22
8: getstatic #2 // Field j:I
11: iconst_1
12: iadd
13: putstatic #2 // Field j:I
16: iinc 1, 1
19: goto 2
22: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
25: getstatic #2 // Field j:I
28: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
31: return
这次我们并没有看到dup指令和两次putstatic了。这也很容易以理解,我们代码怎么写,JVM自然就怎么干。前面我们j=(j++)
这一行,实际就是需要两次putstatic,括号里j++执行后需要putstatic把栈顶的值赋值给j,后面j=(j++)还需要一次putstatic,可实际上栈顶只有一个值,那JVM自然而然就需要dup来复制一份。这里结果的差异就是虚拟机指令dup和完成j++的指令:getstatic、iconst_1、iadd、putstatic的执行先后差异。
静态变量j++的执行顺序是先dup复制,然后再iconst_1、iadd完成自增;那么++j执行顺序可想而知是先iconst_1、iadd完成自增,再dup复制。j–和--j同理,只是iadd指令换为isub而已。
总结
- 局部变量i的自增是指令iinc完成,且是在局部变量表中完成,无需压入操作数栈。
- 静态变量j的自增是由指令getstatic、iconst_1、iadd、putstatic完成,是需要在操作数栈中完成的。
- 静态变量j的j++执行顺序是先dup复制,然后再iconst_1、iadd完成自增。
- 静态变量j的++j执行顺序是先iconst_1、iadd完成自增,再dup复制。
- j–和--j同理,只是iadd指令换为isub而已