likes
comments
collection
share

从字节码层面解析Java语言--i与i--的区别

作者站长头像
站长
· 阅读数 10

概述:

众所周知,--i表示先执行自减运算,然后再使用自减后的i变量值进行其他的运算。i--表示先使用i的值进行运算,然后再对i变量进行自减。相信大家在看各种辅导书的时候,都是这样去死记硬背的,并没有深入探究为什么会这样。

我们先横向比较下其他语言中的--i与i--:

可以肯定的是,基本上大部分语言类型如C、C++、Python、JavaScript等等语言,其执行的逻辑顺序和我开头的描述是一模一样的,只是在实现的原理上略有不同。

像C语言:

(1)i--是先用临时对象保存原来的i变量值,然后原对象自减,再返回临时对象,不能作为左值;但是这种方式由于需要生成临时对象,因此需要调用两次构造函数和析构函数(将原对象赋给临时对象一次,将临时对象以值传递方式返回一次)

(2)--i是直接对原对象进行自减,然后返回原对象的引用,可以做为左值。这种方式不涉及到临时对象,且返回值以引用方式返回,故效率更高

具体可以参考这篇文章:zhuanlan.zhihu.com/p/391942337

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为例

从字节码层面解析Java语言--i与i--的区别 (JVM运行时数据区分布变化情况)

1.线程私有

程序计数器(PC):每个线程一块,指向当前线程正在执行的代码行号。如果当前线程执行的native方法,则返回null。

本地方法栈(Native Method Stack):功能与虚拟机栈类似,不过执行的是native方法。

虚拟机栈(VM Stack):每个java方法在被调用的时候都会被创建一个栈帧(stack frame),并且并且随着线程的生命周期结束而结束。其组成结构如下所示:

从字节码层面解析Java语言--i与i--的区别

2.线程共享

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

从字节码层面解析Java语言--i与i--的区别

字符串常量池:实际也是存放在堆内存中的,存放的字符串常量的实例对象。

堆外内存(本地内存):这里面主要存放的是元数据空间,该空间存放的是方法区的信息,主要包括虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

  • 运行时常量池
    • 仅有类常量池是不够的,因为class常量并不保存方法字段在内存中的布局,因此在JVM运行起来的时候需要有一个运行时常量池,来存储通过class文件常量池构建的运行时常量或者是在运行时产生的新的常量。而且后者(运行时产生的新的常量,也就是非预置入class文件的常量池内容)。比较常用的就是String类的intern()方法.
    • 常量池主要是存放字面量和符号引用
  • 类常量池
    • 每个class文件都会有一个类常量池,存放的是字符串常量、类和接口名字、字段名、和其他一些在class中引用的常量。

附录:

1.JVM常用字节码指令集:blog.csdn.net/wendyyanan/…

2.本文进行字节码反解析使用的IDEA插件是 ASM Bytecode Viewer 大家可以自行在IDEA的plugin市场中进行安装使用。

3.JVM常量池里到底有什么:www.jianshu.com/p/614e2b6a0…

4.JVM详解之运行时常量池:www.cnblogs.com/flydean/p/j…

5.JVM局部变量表:blog.csdn.net/qq_37924905…

6.JVM从代码到机器码:zhuanlan.zhihu.com/p/345076844

7.ITeye www.iteye.com/