从自字节码分析自增运算

class test{
public static void  main(String ...args){
int i = 5;	
 int j = i++;
 int k = ++i;
}
}

 

 


Constant pool:
   #1 = Methodref          #3.#12         // java/lang/Object."<init>":()V
   #2 = Class              #13            // test
   #3 = Class              #14            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               main
   #9 = Utf8               ([Ljava/lang/String;)V
  #10 = Utf8               SourceFile
  #11 = Utf8               test.java
  #12 = NameAndType        #4:#5          // "<init>":()V
  #13 = Utf8               test
  #14 = Utf8               java/lang/Object
{
  test();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=1, locals=4, args_size=1
         0: iconst_5
         1: istore_1
         2: iload_1 //先load到操作数栈
         3: iinc          1, 1 //自增
         6: istore_2 //保存到j
         7: iinc          1, 1     //直接自增
        10: iload_1 //load到操作数栈
        11: istore_3 //保存到k
        12: return
      LineNumberTable:
        line 3: 0
        line 4: 2
        line 5: 7
        line 6: 12
}
SourceFile: "test.java"

更多参考

 

引子

最近重新学习Java,在算术运算符章节里面发现了一个很奇怪的例子,代码如下:

class IncrementByteCode{
    public static void main(String[] args){
        int i = 0;
        i = i++;
        System.out.println(i);
    }
}

按照正常的思路, 第4行代码中,变量i首先进行赋值运算,由于初始值为0,因此i依然为0;然后i自增一次,i由0变成1。第5行,显然输出结果应该为1。但是在实际运行之后得到的结果是0。我看的是某个java视频教程,他给出的解释是,在执行第4行代码时,内部的运行步骤是:先用一个临时的变量temp保存变量自增前的值,然后变量自增,最后自增表达式会把temp的值作为整个表达式的值返回,也就是把temp的值返回给了i,由于temp保存的是i自增前的值,所以i的值不会变。讲道理,这种解释太“任性”了,有种被“钦定”的感觉,所以我并不能接受。于是,我google了一下,看到一篇博客 [1],里面记录了和我一样的问题,并且从bytecode(字节码)的角度,给出了解释。我看了一下内容,不明觉厉,但是那些奇怪的操作码(opcode)并不能看懂,于是一个一个google。虽然每个操作码的含义弄明白了,但是那些个什么栈啊之类的还是不明白是怎么回事,后来发现了上面那篇博客的参考文章 [2],里面给出了Java字节码的简单解释,看完之后才算明白是怎么回事。

文章的余下部分,首先是我看完参考文章后的总结,然后是结合对字节码的理解给出上面例子运行结果的解释。也就说,整篇文章的内容基本来源于两篇博文(我会在最后给出参考列表),我只是在我理解的基础上给出心路历程。

字节码

什么是Java字节码?我们都知道,Java程序的运行步骤是这样的:

27151858_O959.jpg

在JVM(Java虚拟机)里运行的实际上就是字节码,它就相当于C/C++编译后的汇编语言。为了对字节码有个直观的了解,我们先看一段简单的程序生成的字节码是什么样子的。

 

class Test{
    public static void main(String[] args){
        int a = 0;
        System.out.println(a);
    }
}

这段代码所在的文件是test.java,先编译Java文件,然后用javap命令即可从字节码文件中得到具体的字节码。如下:

27151858_mVLI.jpg

上面那段代码里面没有构造函数,所以会默认有一个Test()的构造函数,关于这个构造函数的信息由"Test();"下的四行代码给出。接下来的“public static void main(java.lang.String[]);”下是main函数的具体内容。“Code:”后的代码就是操作码,每个操作码实现特定的功能。

 

为了理解字节码,我们首先需要了解一下随着字节码的执行,JVM是如何工作的。JVM基于栈。在代码的实际运行中,每个线程都用一个JVM栈存储frames。每当有方法调用时,frame就会被创建。一个frame由三部分组成:操作数栈(oprand stack)、局部变量数组、当前方法所在类的运行常量池的引用。局部变量数组存储的是方法的参数和局部变量的值。存储参数的索引从0开始,如果是构造方法或者实例化方法的frame,那么局部变量数组的“0”处存储的是“this”引用,然后再从“1”开始存储形参和局部变量;如果是静态方法的frame,局部变量表不会存储“this”引用,而是从“0”开始存储形参和局部变量。操作数栈临时存储参与运算的数值,然后进行相关操作。至于常量池,在类初始化的时候,会为给出的常量分配一个常量池,并且为每一个常量给出引用。盗用一下博客里面的图,一个frame的图形化描述如下:

27151858_T3gM.jpg

下面,就上一段代码的main函数部分的字节码给出解释(为了大家看得方便,我把原始代码以及生成的main方法的字节码同时放在一个地方):由于main方法是一个静态方法,所以在局部变量表里面“0”处存储args,“iconst_0”将整数0压入操作数栈,“istore_1”将操作数栈的栈顶元素(这里是0),存储到局部变量表的“1”索引处。“getstatic”载入“System.out”域,“iload_1”将局部变量表索引“1”处的值压入操作数栈,然后“invokevirtual”调用println方法,将操作数栈的栈顶元素作为参数传入println方法,并且操作数栈的栈顶pop,最后“return”结束main方法。

 

// 原始代码
public static void main(String[] args){
        int a = 0;
        System.out.println(a);
}
// main方法的字节码
 Code:
       0: iconst_0
       1: istore_1
       2: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
       5: iload_1
       6: invokevirtual #3  // Method java/io/PrintStream.println:(I)V
       9: return

就我目前了解到的,像“istore_1”、“iload_1”这样的操作码,都是针对局部变量表的操作,下划线后面的数字代表局部变量表的索引位置,而“iconst_0”后面的数字代表实实在在的整数值。具体每个操作码的含义,请各位查阅Java字节码参考表 [3]。

还有一个问题:每行操作码前面的数字 0,1,2,5,6,9是什么意思?对于每个方法,都有一个字节码数组,数组的每个元素的大小是一个字节,每个操作码占据一个字节的位置。“iconst_0”占据一个字节的位置,所以“istore_1”到了“1”的位置。同理,“getstatic”到“2”的位置,由于此操作载入了一个域,这个域会占据特定空间(具体多大,自行查阅资料),所以“iload_1”到“5”的位置,依次类推。

自增运算的字节码解释

原始代码和主要的字节码如下:

// 原始代码
class IncrementByteCode{
    public static void main(String[] args){
        int i = 0;
        i = ++i;
        System.out.println(i);
    }
}

// main方法的字节码
public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: iload_1
       3: iinc          1, 1
       6: istore_1
       7: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: iload_1
      11: invokevirtual #3  // Method java/io/PrintStream.println:(I)V
      14: return
}

首先,由于是静态函数,初始时局部变量数组的“0”处存储的是args参数。“iconst_0”将整数0压入操作数栈,此时操作数栈为[0]。然后,“istore_1”将操作数栈的栈顶元素放入局部变量数组的“1”处,局部变量数组的内容是[args,0],此时操作数栈是[](也就是空栈)。接着,“iload_1”将局部变量数组里的“1”处的值压入操作数栈,操作数栈变成[0]。接下来注意了,“iinc”操作码是直接把局部变量数组的“1”处的值增加1,也就是说局部变量数组变成了[args,1]。接着,“istore_1”将操作数栈的栈顶元素,也就是0,放入局部变量数组的“1”处,局部变量数组又变成了[args,0]。“getstatic”载入“System.out”域,然后“iload_1”将局部变量表里“1”处的值(也就是0)压入操作数栈,作为参数传入“println”函数输出到命令行。也就是最终输出结果为0。

为了更加理解自增运算,我们看看下面这段代码:

// 原始代码
class IncrementByteCode{
    public static void main(String[] args){
        int i = 0;
        i = i++;
        System.out.println(i);
    }
}

// main方法的字节码
public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: iinc          1, 1
       5: iload_1
       6: istore_1
       7: getstatic     #2  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: iload_1
      11: invokevirtual #3  // Method java/io/PrintStream.println:(I)V
      14: return
}

与之前代码不同之处在于,源代码的i从先赋值再再增变成了先自增再赋值。对应的字节码中,先是在局部变量数组里面进行自增运算,然后将得到结果(1)压入操作数栈,最后再保存到局部变量表里面的就是1了,输出结果当然也是1。

其实,问题的关键在于自增运算发生的地方:局部变量数组!正常情况下,数值的操作应该都在操作数栈里完成。可能是为了减少运行时间吧。 

总结

本来都没把自增运算当回事,因为学过太多语言了,几乎每一门语言都会有自增运算。由这么一个小小的例子,发现了字节码的用处,确实很有意思。


参考文献

1. [转]深入理解自增自减运算符a=a++和a=++a

2. Java bytecode:

3. Java Virtual Machine

https://zhuanlan.zhihu.com/p/22556307

转载于:https://my.oschina.net/sfshine/blog/2209481

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值