从字节码层面解析Java语言--i与i--的区别
概述:
众所周知,--i表示先执行自减运算,然后再使用自减后的i变量值进行其他的运算。i--表示先使用i的值进行运算,然后再对i变量进行自减。相信大家在看各种辅导书的时候,都是这样去死记硬背的,并没有深入探究为什么会这样。
我们先横向比较下其他语言中的--i与i--:
可以肯定的是,基本上大部分语言类型如C、C++、Python、JavaScript等等语言,其执行的逻辑顺序和我开头的描述是一模一样的,只是在实现的原理上略有不同。
像C语言:
(1)i--是先用临时对象保存原来的i变量值,然后原对象自减,再返回临时对象,不能作为左值;但是这种方式由于需要生成临时对象,因此需要调用两次构造函数和析构函数(将原对象赋给临时对象一次,将临时对象以值传递方式返回一次)
(2)--i是直接对原对象进行自减,然后返回原对象的引用,可以做为左值。这种方式不涉及到临时对象,且返回值以引用方式返回,故效率更高
JAVA语言对--i与i--的字节码实现原理:
先上一个简单的demo
public static void main(String[] args) { int i = 9999; if (--i >= 9999) { System.out.println("--i>=9999".concat(i+"")); } int j = 99; if (j-- >= 99) { System.out.println("j-->=99".concat(j+"")); } }
如下是将字节码解析成JVM指令助记符的结果
// access flags 0x21 public class algorithm/test/DoubleListInsert { // compiled from: DoubleListInsert.java // access flags 0x1 public <init>()V L0 LINENUMBER 6 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V RETURN L1 LOCALVARIABLE this Lalgorithm/test/DoubleListInsert; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1 // access flags 0x9 public static main([Ljava/lang/String;)V L0 LINENUMBER 8 L0 SIPUSH 9999 ISTORE 1 L1 LINENUMBER 9 L1 IINC 1 -1 ILOAD 1 SIPUSH 9999 IF_ICMPLT L2 L3 LINENUMBER 10 L3 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "--i>=9999" NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V ILOAD 1 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; LDC "" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKEVIRTUAL java/lang/String.concat (Ljava/lang/String;)Ljava/lang/String; INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L2 LINENUMBER 13 L2 FRAME APPEND [I] BIPUSH 99 ISTORE 2 L4 LINENUMBER 14 L4 ILOAD 2 IINC 2 -1 BIPUSH 99 IF_ICMPLT L5 L6 LINENUMBER 15 L6 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "j-->=99" NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V ILOAD 2 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; LDC "" INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKEVIRTUAL java/lang/String.concat (Ljava/lang/String;)Ljava/lang/String; INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L5 LINENUMBER 17 L5 FRAME APPEND [I] RETURN L7 LOCALVARIABLE args [Ljava/lang/String; L0 L7 0 LOCALVARIABLE i I L1 L7 1 LOCALVARIABLE j I L4 L7 2 MAXSTACK = 4 MAXLOCALS = 3 }
通过指令助记符的解析结果我们可以看出,--i是先进行了IINC操作(将局部变量表中的整数9999进行-1运算,然后执行ILOAD操作(也就是将变量i从局部变量中重新加载到栈中),这样在后面进行if条件判断的时候,实际i是陨石后最新的值也就是9998.
而i--则是先执行了ILOAD操作(也就是将变量j从局部变量表中压入到当前栈中),然后再进行IINC(也就是将局部变量表中的99进行-1运算,注意此时方法栈中的变量值还是99),因此在后续的if条件判断时,仍然是将99与类常量池中的99进行比较)。需要注意的是在后续使用j变量时,需要重新执行ILOAD操作,这样j就是最新的值了。
拓展知识
(1)JVM的内存分区划分,以JDK1.8为例

(JVM运行时数据区分布变化情况)
1.线程私有
程序计数器(PC):每个线程一块,指向当前线程正在执行的代码行号。如果当前线程执行的native方法,则返回null。
本地方法栈(Native Method Stack):功能与虚拟机栈类似,不过执行的是native方法。
虚拟机栈(VM Stack):每个java方法在被调用的时候都会被创建一个栈帧(stack frame),并且并且随着线程的生命周期结束而结束。其组成结构如下所示:

2.线程共享
堆内存(heap):该区域是JVM中容量最大、管理最复杂的区域,也是GC回收最主要的地方。其唯一用途就是存放创建的对象实例、数组对象等。注意字符串常量池子JDK1.7之后就从永久代中移入到堆内存的运行时常量池中了。

字符串常量池:实际也是存放在堆内存中的,存放的字符串常量的实例对象。
堆外内存(本地内存):这里面主要存放的是元数据空间,该空间存放的是方法区的信息,主要包括虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- 运行时常量池
- 仅有类常量池是不够的,因为class常量并不保存方法字段在内存中的布局,因此在JVM运行起来的时候需要有一个运行时常量池,来存储通过class文件常量池构建的运行时常量或者是在运行时产生的新的常量。而且后者(运行时产生的新的常量,也就是非预置入class文件的常量池内容)。比较常用的就是String类的intern()方法.
- 常量池主要是存放字面量和符号引用
- 类常量池
- 每个class文件都会有一个类常量池,存放的是字符串常量、类和接口名字、字段名、和其他一些在class中引用的常量。