zoukankan      html  css  js  c++  java
  • java字节码角度图解 i++ 和 ++i

    从本科时期学C语言的时候,我们就被教导:

    ++i 是先自增,后赋值

    i++ 是先赋值,后自增

    最近在看jvm虚拟机和java并发编程,希望从字节码的角度来进行介绍。

    其实这个细节写的人已经不少了,但本人还是想稍微综合一下,更通俗、更直观地进行描述。

    阅读本文需要对Java 虚拟机(Java Virtual Machine,JVM)有一定的了解,至少需要了解字节码指令(bytecode instruction)的含义以及方法在JVM虚拟机栈(JVM Stack)的执行过程。具体可以参阅《深入理解Java虚拟机:JVM高级特性与最佳实践》。

    浏览下文之后,你将会理解(而不是背诵)下列代码的运行结果:

    代码片段摘自于java中i++ 和 ++i的区别

    int a = 0;
    for(int i = 0; i < 99; i++){
        a = a++;
    }
    System.out.println(a); // 输出0
    
    int b = 0;
    for (int i = 0; i < 99; i++) {
        b = ++ b;
    }
    System.out.println(b); // 输出99
    
    int a = 0;
    int b = 0;
    for (int i = 0; i < 99; i++) {
    	a = a++;
    	b = a++;
    }
    System.out.println(a); // 输出99
    System.out.println(b); // 输出98
    

    字节码和虚拟机

    java中的字节码指令就相当于c/c++中的机器码。

    开发者编写源文件(*.java),通过javac命令将源文件编译为可被JVM所能理解的class文件,其中包含了该类的常量,接口,字段,方法等信息。

    对于每一个方法,其包含了行号表(LineNumber Table),局部变量表(LocalVariable Table),字节码,异常表(Exception Table)等信息。

    使用JDK下的javap命令或者jclasslib bytecode viewer 软件可以查看字节码信息。

    每一个方法都由若干字节码指令组成,指令会对当前方法的局部变量(Local Variables)和操作数栈(Operand Stack)产生影响。注意,一个方法体内仅维护一个局部变量表和操作数栈;操作数栈保存了当前的变量的值,其中的元素仅通过入栈(push)和出栈(pop)两种操作进行。

    具体的指令含义,可参阅Oracle官方的JVM 规范中有关指令的说明Chapter 6. The Java Virtual Machine Instruction Set

    指令名称 操作数栈的变化 描述
    iconst_<i> i 入栈 把整数i放入栈中
    istore_<i> 顶部元素出栈(pop) 将栈顶的整数弹出,并且赋值给局部变量表中的index为i的元素
    iload_<n> 局部变量表的中元素入栈(push) 将局部变量表中index为n的元素放入栈中
    iinc i by n >无变化 对局部变量表中index为i的元素加上n
    iadd 栈顶弹出两个元素,并返回一个元素(pop, pop, push) 从栈顶连续弹出两个元素,将其相加后的结果放回栈中
    getstatic<i> 将符号引用i放入栈中 将符号引用i放入栈中
    invokevirtual<n> ..., objectiveref, [arg1, [arg2...]] -> ... (pop, pop, pop ,...) 调用栈中对象objectiveref的方法n,并以arg1, arg2为该方法的参数

    此处需要重点注意的是,iinc自增指令直接对局部变量表的元素进行累加,而不是在栈中。

    如果不了解指令集的读者可能还云里雾里的,没关系,接下来将会用图解的方法进行介绍。

    单独的i++与++i运算

    如果仅有i++++i ,如:

    int i = 0;
    i++;
    
    int i = 0;
    ++i;
    

    其字节码指令是相同的,均为:

    0 iconst_0
    1 istore_1
    2 iinc 1 by 1
    5 return
    

    单独的i++和++i

    该图描述了上述代码的字节码执行机制:第0行为向操作数栈顶添加0;第1行为将栈顶的元素存至局部变量表的index为1处的地方,也就是i;第2行为自增操作,注意此处没有栈的操作,在局部变量表直接将index为1的变量加1;第5行为return语句,无返回值。

    通过上述的介绍,读者可能大概了解到了字节码的执行原理,注意的是此处不管是i++还是++i,其字节码都是一样的。就好像我们在写for(int i = 0; i<len; i++/++i)都可以

    i++和++i出现赋值的情况

    我们来看一下出现i++赋值的情况:

    int i = 0;
    i = i++; 
    System.out.println(i); // i = 0
    

    如何去理解自己赋值的时候i最后还是等于0呢?其字节码如下所示(不包含print输出语句):

    0 iconst_0
    1 istore_1
    2 iload_1
    3 iinc 1 by 1
    6 istore_1
    7 return
    

    i++赋值的字节码流程

    由字节码指令可以看出,当i++;变为i=i++;后,第2行通过load命令先将i的值拷贝至栈顶,然后在第3行对i进行自增,但此时自增在局部变量表中执行0->1,但是在栈中的值不变;之后在第6行上,再次将栈顶的值赋给了局部变量表中index为1,也就是赋给变量i,因此i仍然为0。

    这样的结果仍然于i++的先赋值,后自增不冲突。i先复制了一份值在栈顶,自增,只不过再把栈顶赋值给左端项i正好还是自己。

    接下来看一下++i赋值的情况:

    int i = 0;
    i = ++i;
    System.out.println(i); // i = 1
    

    其字节码如下所示(不包含print输出语句):

    0 iconst_0
    1 istore_1
    2 iinc 1 by 1
    5 iload_1
    6 istore_1
    7 return
    

    1586612151133

    由字节码可以看出,在i=++i中,第2行为自增后,局部变量表中i变为1,之后第5行才从变量表中将i加载出来,第6行再赋值回去。与此同时,i=++i;这行语句还会提示“The assignment to variable i has no effect”的信息,意为这样的赋值是没有意义的,正如字节码解释,我们把i的值加载出来,又赋值回去。

    由以上对比可知,在赋值的情况下:

    i++是先加载原值到栈中,自增变量表,再把栈中的值赋给相应变量;

    ++i是先自增变量表,加载其到栈中,再把栈中的值赋给相应变量。

    其他情况举例

    例1(选自用java字节码解释i++和++i ):

    int i = 0;
    int j = 0;
    j = i++ + i++;
    System.out.println(j); //输出j=1
    System.out.println(i); //输出i=2
    

    为什么j=1, i=2呢?字节码解释为:

     0 iconst_0
     1 istore_1
     2 iconst_0
     3 istore_2
     4 iload_1
     5 iinc 1 by 1
     8 iload_1
     9 iinc 1 by 1
    12 iadd
    13 istore_2
    14 return
    

    例1字节码流程

    由字节码可知,第0-3行为初始化ij的值,每次计算i++时,都是先把i的旧值加载到栈中,然后i自增。第1次i++时,栈中的i=0,局部变量表为i=1;第2次i++时,栈中载入的就是自增后的i,即1,之后i在变量表中再次自增,此时i=2。最后要计算i++ + i++的值,因此iadd指令将栈中两个元素连续弹出相加得1,再将其返回值栈顶,最后istore指令将该值赋给j,运算结束。因此,我们最终得到了i=2,j=0+1=1的结果。

    例2:

    int i = 0;
    int j = 0;
    j = i++ + i++ + i++;
    

    字节码文件如下:

     0 iconst_0
     1 istore_1
     2 iconst_0
     3 istore_2
     4 iload_1
     5 iinc 1 by 1
     8 iload_1
     9 iinc 1 by 1
    12 iadd
    13 iload_1
    14 iinc 1 by 1
    17 iadd
    18 istore_2
    19 return
    

    该运算结果为i=3,j=3

    根据前一案例可以推导出: i++计算了3次,因此i=3; j=0 + 1 + 2 =3

    例3:从JVM角度看i++ 与++i

    int i = 0;
    i = i++ + ++i; //i最终为2
    

    字节码为:

     0 iconst_0
     1 istore_1
     2 iload_1
     3 iinc 1 by 1
     6 iinc 1 by 1
     9 iload_1
    10 iadd
    11 istore_1
    12 return
    

    例3字节码流程

    具体执行流程我就不写了,相信大家看图肯定能理解的。

    小结

    开头的例子我也略过去了,大家如果仔细推到的话,肯定也能推出来的。

    以前总觉得讨论 i++ 或者 ++i等一些考题就好像“茴”的几种写法一样无聊,但直到看开Java虚拟机和并发之后,才真的明白了为什么i++操作不具备原子性,就是因为其在指令码中并不是一条指令,而是具有读取,增加,赋值三种操作的。

  • 相关阅读:
    动手动脑篇之类与对象
    团队精神
    在快乐中学习
    实习报告
    大道至简读后感(二)
    大道至简读后感
    读《大道至简》第一章有感
    指令随笔之:tail、cat、scp、&、&&、;、|、>、>>
    NFS安装过程
    CentOS7编译安装Nginx-1.8.1和编译参数
  • 原文地址:https://www.cnblogs.com/TianYuanSX/p/12682824.html
Copyright © 2011-2022 走看看