从字节码理解Java中局部变量的自增/自减

戏子无情 提交于 2020-07-24 11:47:13

Java中的i++和++i,你真的懂了吗?

思考一下下面这段代码输出什么?

public static void main(String[] args) {
        int j=0;
        for(int i=0;i<10;i++){
            j=(j++);
        }
        System.out.println(j);
    }

相信你已经有了答案,上面这段代码输出的是10吗?正确答案是0。那么就要思考一下,代码的执行顺序,难道不是j++后再赋值给j吗?答案当然是啊。那为什么结果会是0呢?要解答这个疑惑,就要了解一下栈帧中的局部变量表和操作数栈。关于JVM的内存结构,可以到我的上一篇博客了解。

字节码

一个简单的Helloworld.java文件

public class Helloworld {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}

经过编译成Helloworld.class后是这样的:
在这里插入图片描述
从这些字节码中理解java中的操作指令,这不是开玩笑吗?这哪是给人看的啊!!对,这还真不是给人看的,这是给机器看的。那人要用什么看呢?Oracle提供javap工具来反编译*.class文件,IDEA中可以通过下图操作使用javap工具。
在这里插入图片描述
键入javap -v -p *.class文件所在路径,回车
在这里插入图片描述




Classfile /D:/IDEA/WorkSpace/out/production/JavaBasic/self_increment/Test1.class
  Last modified 2020530; size 614 bytes
  SHA-256 checksum f2226c103b1fcddfbc1c883ca6ae2a58564043c8522365647b306e4376596cad
  Compiled from "Test1.java"
public class self_increment.Test1
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #4                          // self_increment/Test1
  super_class: #5                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #5.#23         // java/lang/Object."<init>":()V
   #2 = Fieldref           #24.#25        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Methodref          #26.#27        // java/io/PrintStream.println:(I)V
   #4 = Class              #28            // self_increment/Test1
   #5 = Class              #29            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lself_increment/Test1;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               i
  #16 = Utf8               I
  #17 = Utf8               args
  #18 = Utf8               [Ljava/lang/String;
  #19 = Utf8               j
  #20 = Utf8               StackMapTable
  #21 = Utf8               SourceFile
  #22 = Utf8               Test1.java
  #23 = NameAndType        #6:#7          // "<init>":()V
  #24 = Class              #30            // java/lang/System
  #25 = NameAndType        #31:#32        // out:Ljava/io/PrintStream;
  #26 = Class              #33            // java/io/PrintStream
  #27 = NameAndType        #34:#35        // println:(I)V
  #28 = Utf8               self_increment/Test1
  #29 = Utf8               java/lang/Object
  #30 = Utf8               java/lang/System
  #31 = Utf8               out
  #32 = Utf8               Ljava/io/PrintStream;
  #33 = Utf8               java/io/PrintStream
  #34 = Utf8               println
  #35 = Utf8               (I)V
{
  public self_increment.Test1();
    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/Test1;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_0
         3: istore_2
         4: iload_2
         5: bipush        10
         7: if_icmpge     21
        10: iload_1
        11: iinc          1, 1
        14: istore_1
        15: iinc          2, 1
        18: goto          4
        21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: iload_1
        25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        28: return
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 10
        line 6: 15
        line 9: 21
        line 10: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            4      17     2     i   I
            0      29     0  args   [Ljava/lang/String;
            2      27     1     j   I
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 4
          locals = [ int, int ]
        frame_type = 250 /* chop */
          offset_delta = 16
}
SourceFile: "Test1.java"

反编译后,就是给人看的了。我们可以看到,类文件里面有Constant pool(常量池),构造方法public self_increment.Test1()和main方法public static void main(java.lang.String[])中的指令集,LocalVariableTable(局部变量表)等等。

JVM虚拟机指令

JVM虚拟机指令繁多,这里只把用到的解释一下。

  • iconst_0:将值为0的int类型推送至栈顶,(0~5通过该指令,-1通过指令iconst_m1)。
  • istore_1:将栈顶的值存入slot为1,即1号槽位中。
  • iload_2:将局部变量表2号槽位中的值压入栈顶。
  • bipush :将单字节常量(-128~127)压入栈顶。
  • if_icmpge :比较栈顶两个int类型的数值,当前者大于等于后者时跳转。
  • iinc :完成int类型数值自增自减。
  • getstatic:获取指定类的静态字段,并将值压入栈顶。
  • invokevirtual:虚方法调用。
    了解了这些内容,就可以真正开始解答疑惑了。

分析

我们主要关注main方法中代码的执行,随着main方法的调用,就会在java虚拟机栈创建一个栈帧并入栈。main方法栈帧中包括操作数栈和局部变量表,当然还有其他,这里不多阐述。

 LocalVariableTable:
        Start  Length  Slot  Name   Signature
            4      17     2     i   I
            0      29     0  args   [Ljava/lang/String;
            2      27     1     j   I

我们看到,局部变量表中有三个Slot(槽位),槽位中存储的值就是对应的变量名的值。

         0: iconst_0
         1: istore_1
         2: iconst_0
         3: istore_2
         4: iload_2
         5: bipush        10
         7: if_icmpge     21
        10: iload_1
        11: iinc          1, 1
        14: istore_1
        15: iinc          2, 1
        18: goto          4
        21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
        24: iload_1
        25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        28: return

mian方法中的一系列虚拟机指令,都是在操作数栈完成的。
我们逐条分析指令

  • iconst_0,将值为0的int类型压入栈顶。
  • istore_1,将栈顶的值出栈并存入1号槽位,即j=0,对应源码中将局部变量j初始化为0。
  • iconst_0,同上。
  • istore_2,同理,i=0。
  • iload_2,将二号槽位上i的值入栈。
  • bipush 10,将10压入栈顶。
  • if_icmpge 21,比较栈顶两个int类型的值,当前者(先入栈的i)大于等于后者(后入栈的10),跳转到21行。显然这是for循环条件不满足后跳出,条件满足时则继续往下走。
  • iload_1,将1号槽位上,即j的值入栈。
  • iinc 1, 1,将1号槽位,即j的值+1,现在j=1。
  • istore_1,将栈顶的值,即0存入1号槽位,可以看到,这一步将j的值重新赋值为0。
  • iinc 2, 1,将2号槽位,即i的值+1。
  • goto 4,跳转到第四行。
  • getstatic #2,根据#2在常量池中找,javap工具给出的注释内容即#2在常量池对应的是System.out对象,它的类型是PrintStream。
  • iload_1,将1号槽位j的值入栈。
  • invokevirtual #3 ,根据#3在常量池中找,它对应的是println:(I)V,参数是I,即Integer。
  • return,main方法结束,对于的栈帧出栈。

我们可以看到,在每一次的for循环中,局部变量表中j的值+1后,istore_1这条指令将栈顶的0重新存入j中,每一次for循环后,j的值都是0。
在这里插入图片描述
③istore_1,就将栈顶的0出栈并存入局部变量表的1号槽位,即j中。

思考

将文章开头那段代码中j=(j++)换为j=(++j),结果又是什么?

总结:

  1. iinc这个指令是在局部变量表中完成,无需压入操作数栈。
  2. 对于++i这个操作,JVM先执行iinc指令再执行iload指令。
  3. 对于i++这个操作,JVM先执行iload指令再执行iinc指令。
  4. 对于–i和i–,与++i和i++同理,只是iinc指令后的第二个数为-1,表示自减而已。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!