从字节码理解静态变量与局部变量自增自减的区别

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 2020530; 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而已。

总结

  1. 局部变量i的自增是指令iinc完成,且是在局部变量表中完成,无需压入操作数栈。
  2. 静态变量j的自增是由指令getstatic、iconst_1、iadd、putstatic完成,是需要在操作数栈中完成的。
  3. 静态变量j的j++执行顺序是先dup复制,然后再iconst_1、iadd完成自增。
  4. 静态变量j的++j执行顺序是先iconst_1、iadd完成自增,再dup复制。
  5. j–和--j同理,只是iadd指令换为isub而已
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值