从字节码的角度分析i++和++i的本质区别

       最近在研究广播机制的时候,碰到了一个i++类型的问题,代码节选如下:

......
r.nextReceiver = 0;
int recIdx = r.nextReceiver++;
......

       在这两行代码中,因为错误的认为recIdex的结果是1,导致下面的广播发送流程理解错误,考虑到这个问题很典型,代码中经常用到,面试笔试也经常碰到,所以从字节码入手,分析下i++的原理,顺便再分析下++i的原理,然后举一个xx公司的笔试题增加理解,比如有如下代码:

public class TopwiseAdd{
    public static void main(String[] args){
        TopwiseAdd ta = new TopwiseAdd();
        int a = ta.add(2);
        System.out.println(a);
    }

    public int add(int i){
        i = i++;
        return i;
    }
}

       这里定义了一个add方法,传入一个int型的参数,在方法体里面将参数自增,然后返回;在main方法里面调用该方法,传入2,代码非常简单。下面通过javac -g TopwiseAdd.java来编译该类,然后通过javap -verbose TopwiseAdd
这个命令来查看add方法的字节码(其他的字节码没有贴出来),如下所示:

public int add(int);
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iload_1
         1: iinc          1, 1
         4: istore_1
         5: iload_1
         6: ireturn
      LineNumberTable:
        line 9: 0
        line 10: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
         0       7      0   this   LTopwiseAdd;
         0       7      1     i      I

       首先来解释下这个字节码。
  1.flags:ACC_PUBLIC,说明此方法是public类型的;
  2.code:就是方法体,不过是用虚拟机指令来表示的,其中stack=1是说该方法的操作数栈的深度是1,通俗点说就是他的操作数栈只能保存一个变量或者中间结果,反正只能保存一个值;locals=2是说该方法包含两个局部变量;args_size=2是说该方法有两个参数,第一个参数是this,第二个参数是i;0,1,4,5,6是虚拟机要执行的指令集。
  3. LocalVariableTable:本地变量表,我们只需要关注Slot,Name和Signature即可。Slot = 0那行代表本地变量表的第一个参数,参数名字是this,参数类型是TopwiseAdd,前面的L代表该参数是引用类型,这就是大名鼎鼎的this变量;每个实例方法都会隐式的包含一个this参数,代表调用该方法的对象,这一点非常重要;如果是类方法的话,就不会有这个参数;Slot = 1那行代表第二个参数,名字是i,类型是整形(I代表整形)

       在方法执行之初。该线程的虚拟机栈分布如下:
1.png
       下面执行第一条指令:
  0:iload_1:这表指令代表将局部变量表的第2个参数压入虚拟机栈;i代表入栈的是整形变量;load是指从局部变量表中获取指定的值,并压入操作数栈;_1代表将局部变量表的第2个变量入栈,0是第一个局部变量,专指this,所以1就指第二个局部变量。此时该线程的虚拟机栈分布如下:
2.png
       1: iinc 1, 1 : 这行指令的作用是将局部变量表的第二个变量+1。注意,iinc应该是多个指令的集合,查看上面的字节码,iinc是第二条指令,下一个指令的编号就变成4了,说明中间还要两条指令,目前还不知道怎么把这两条指令显示出来,java虚拟机规范也没有说明。此时该线程的虚拟机栈分布如下:
3.png

       4: istore_1:将操作数栈顶的值出栈并存入局部变量表的第二个变量,也就是将2存入i,可以看到,局部变量表的i=3被覆盖成i=2了,也就是说i++相当于根本没有执行。此时该线程的虚拟机栈分布如下:
4.png
       5: iload_1:上面说过该指令,将将局部变量表的第2个参数压入虚拟机栈。此时该线程的虚拟机栈分布如下:
5.png
       6: ireturn:方法返回,将操作数栈顶的值返回给方法调用者,结果返回的是2
  从上面的分析中可以看出,i++的结果是i没有变。下面把i = i++改成i = ++i;编译后的字节码如下所示:
    public int add(int);
      flags: ACC_PUBLIC
      Code:
        stack=1, locals=2, args_size=2
           0: iinc          1, 1
           3: iload_1
           4: istore_1
           5: iload_1
           6: ireturn
        LineNumberTable:
          line 9: 0
          line 10: 5
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
            0       7     0    this   LTopwiseAdd;
            0       7     1      i        I

       可以看出,这此的字节码跟之前的字节码有明显的区别,主要体现在第一条指令,这里的第一条指令是iinc,也就是说,对于++i这种操作,虚拟机并不会将该变量压入栈顶,而是直接把局部变量表的变量加1,以后的任何操作,包括出栈入栈,都是基于自增后的结果来操作的。刚进入该方法时,该线程的虚拟机栈分布如下:
6.png

       下面逐条分析该字节码的指令:

       0: iinc 1, 1:上面说过,将第2个局部变量的值加1.此时该线程的虚拟机栈分布如下:
7.png
       3: iload_1:将第二个局部变量压入操作数栈。此时该线程的虚拟机栈分布如下:
8.png
       4: istore_1:将栈顶的操作数出栈,并存入第二个局部变量。此时该线程的虚拟机栈分布如下:
9.png

       5: iload_1:将第二个局部变量压入虚拟机栈。此时该线程的虚拟机栈分布如下:
10.png
       6: ireturn:将栈顶的操作数出栈,并将该操作数返回给方法调用者
       从上面两个分析可以看出,虚拟机执行字节码时,总是将操作数在局部变量表和操作数栈之间来回转移。i++的原理是先将局部变量表的变量入栈,然后局部变量本身自增,接着将栈顶的操作数保存到局部变量表(也就是覆盖自增操作),其结果是自增的结果被还原了;++i的原理是上来就将局部变量表的变量自增,然后入栈,接着不管在栈顶做什么操作,都是基于自增后的值来操作的,而i++都是基于自增前的值来操作的
       从上面的分析来看,i++好像也没什么用啊,那为什么要搞i++这种玩意呢?想想,如果i++没用的话,那么常规的for循环是怎么循环下去的呢?既然常规的for循环能够让i真正的自增而没用被覆盖还原,那么说明i++有时候还是能够自增的,为了说明这个问题,下面把上面的代码改成下面的:
public class TopwiseAdd{
    public static void main(String[] args){
        TopwiseAdd ta = new TopwiseAdd();
        int a = ta.add(2);
        System.out.println(a);
    }

    public int add(int i){
        i++;
        return i;
    }
}

       这里唯一的修改就是把i = i++改成了i++,也就是把赋值运算给拿掉了,那么结果呢?我既然特意这样改了,说明结果肯定发生了变化,上面这段语句打印出来是3,下面看下这段代码add方法的字节码(这里就不画图了):

  public int add(int);
    descriptor: (I)I
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iinc          1, 1
         3: iload_1
         4: ireturn
      LineNumberTable:
        line 11: 0
        line 12: 3
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
          0       5     0    this   LTopwiseAdd;
          0       5     1      i        I

       这段字节码更简单,下面逐一分析:
       0: iinc 1, 1:将局部变量表的第二个局部变量(2)自增,这个指令执行完成后,局部变量表的i就等于3了
       3: iload_1:将局部变量表的第二个局部变量压入栈顶,也就是将3压入栈顶
       4: ireturn:栈顶的值出栈,也就是将3返回给main方法
       两个i++调用,区别是一个++后有赋值操作:i = i++;另外一个没有赋值操作,而是直接返回:i++;可是两个结果却不一样,从字节码可以看出:
              a.对于i = i++;这种代码,虚拟机首先将i压入栈顶(上例中的2),然后i自增,这样局部变量表的i变成了3;接着赋值,赋值是把栈顶的值赋值给局部变量表,也就是把2存入局部变量表,所以i = i++的结果是2;
              b.对于i++;这种代码,从字节码来看,并不会在自增前将i压入栈顶,所以也就不存在覆盖操作;自增后i是多少就是多少;这也是i++能够驱动for循环的原因
     假设有如下代码:

public int add(int i){
    int a = 5;
    i++;
    int a = i*2;                                                                                                                                
    return a;
}

     假设传入的i是2,i++后,局部变量表的i是3,因为i++没有赋值操作,所以方法执行之初病不会将i压入栈顶,当然也不存在覆盖还原局部变量表的i的可能;然后i * 2的时候会把局部变量表的i压入栈顶(3),3*2 = 6,结果是6.

     当然了,上面的两个例子是改革开放以来,最简单的i++和++i的例子,下面来看下一个复杂点的例子,这个例子是xx的笔试题,代码如下:

public class CyAdd{
    public static void main(String[] args){
        getValue(2);
    }

    public static int getValue(int i){
        int result = 0;
        switch(i){
            case 1:
                result = result + i;
            case 2:
                result = result + i*2;
            case 3:
                result = result++;
            default:
                ++result;
        }
        return result;
    }
}

     原始的代码只有getValue,问传入2,最后返回的是什么。我们通过上面相同的编译命令和字节码查看命令可以看到下面的字节码(只关注getValue方法):

     public static int getValue(int);
       descriptor: (I)I
       flags: ACC_PUBLIC, ACC_STATIC
       Code:
         stack=3, locals=2, args_size=1
            0: iconst_0
            1: istore_1
            2: iload_0
            3: tableswitch   { // 1 to 3
                          1: 28
                          2: 32
                          3: 38
                    default: 43
               }
           28: iload_1
           29: iload_0
           30: iadd
           31: istore_1
           32: iload_1
           33: iload_0
           34: iconst_2
           35: imul
           36: iadd
           37: istore_1
           38: iload_1
           39: iinc          1, 1
           42: istore_1
           43: iinc          1, 1
           46: iload_1
           47: ireturn
         LineNumberTable:
           line 7: 0
           line 8: 2
           line 10: 28
           line 12: 32
           line 14: 38
           line 16: 43
           line 18: 46
         LocalVariableTable:
           Start  Length  Slot  Name   Signature
             0      48     0     i         I
             2      46     1   result      I

     大部分字节码上面分析过,不过需要特别注意的一点是,LocalVariableTable的第一个参数不是this了,原因是getValue是个类方法,类方法是不需要this的,这点要特别注意。这个方法有两个局部变量,一个是i,一个是result;操作数栈的深度是3;方法执行之初,该线程的虚拟机栈分布如下:
11.PNG

     下面一条条分析字节码指令:

     0: iconst_0 : 将常量0压入操作数栈,此时该线程的虚拟机栈分布如下:
12.PNG
     1: istore_1 :将栈顶的元素出栈,并存入局部变量表的第二位,0,1两条指令对应的代码其实就是int result = 0;,此时该线程的虚拟机栈分布如下:
13.PNG
     2: iload_0 :将局部变量表的第一位压入操作数栈,此时该线程的虚拟机栈分布如下:
14.PNG
    3: tableswitch   { // 1 to 3
                1: 28
                2: 32
                3: 38
            default: 43
        }

     这是switch编译出来的字节码,意思是判断栈顶元素的值,如果是1的话,就执行第28行字节码;如果是2的话,就执行第32行字节码;如果是3的话,那么执行第38行字节码;如果都不是,那么执行第43行字节码,也就是default分支,然后将i出栈。因为我们传入的是2,所以应该执行第32行字节码

     32: iload_1 : 将局部变量表的第二位压入操作数栈,此时该线程的虚拟机栈分布如下:
15.PNG

     这条指令有什么作用呢?此时我们是在执行case 2分支,该分支的代码是:
result = result + i*2;

     可以看到,我们是要将result和i2相加,所以首先我们要把result压入操作数栈,这条指令的作用就是将result压入操作数栈。
     33: iload_0 : 将局部变量表的第一位压入操作数栈,此时该线程的虚拟机栈分布如下:

16.PNG
     34: iconst_2 : 将常量2压入操作数栈,这里的2就是i2时候用到的2,此时该线程的虚拟机栈分布如下:
17.PNG
     35: imul : 该指令的作用是将栈顶的两个元素相乘并出栈,对应的代码就是i*2,此时该线程的虚拟机栈分布如下:
18.PNG
     36: iadd : 将栈顶的两个元素相加,对应的代码就是result+(i * 2)的结果,此时该线程的虚拟机栈分布如下:
19.PNG
     37: istore_1 : 将操作数栈顶的元素存入局部变量表的第二位,此时该线程的虚拟机栈分布如下:
20.PNG
     指令执行到这里,result = result + i * 2;算是执行完毕了。
     38: iload_1 : 将局部变量表的第二位压入操作数栈,此时该线程的虚拟机栈分布如下:
21.PNG
     39: iinc 1, 1 : 将局部变量表的第二位+1,此时该线程的虚拟机栈分布如下:
22.PNG
     42: istore_1 : 将操作数栈顶的元素存入局部变量表的第二位(自增被覆盖还原了),此时该线程的虚拟机栈分布如下:
23.PNG
     ps : 38-42条指令代表的代码是case 3里面的result = result++;
     43: iinc 1, 1 : 将局部变量表的第二位+1,此时该线程的虚拟机栈分布如下:
24.PNG
     46: iload_1 : 将局部变量表的第二位压入操作数栈,此时该线程的虚拟机栈分布如下:
25.PNG
     ps : 43-46条指令代表的代码是default的++result;
     47: ireturn : 将操作数栈顶的元素出栈,并返回给方法调用者
     至此,getValue(2)的流程执行完毕,此问题的答案是5
     这个问题主要考察两个知识点:1,i++和++i的区别,区别在上面总结过;2,在case中如果没有break,代码的执行流程,如果switch的case没有break,代码会继续执行后面的case中指定的代码,一直遇到一个break或者default,这种破问题在笔试中经常碰到
     注意:这是在Oracle的Hotspot虚拟机上执行的过程,也就是说是在纯Java的环境下执行的。对于非纯Java环境下执行的,比如ART虚拟机,Dalvik虚拟机环境下,虽然结果是一样的,但是执行流程是有根本上的区别的,Hotspot虚拟机是基于操作数栈的,而ART和Dalvik是基于寄存器的,对于这种基于寄存器的虚拟机,暂时还不了解,等了解了再来看下其执行流程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容