从字节码层面解析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中引用的常量。
#Java##程序员#
全部评论

相关推荐

04-30 21:35
已编辑
长安大学 C++
晓沐咕咕咕:评论区没被女朋友好好对待过的计小将可真多。觉得可惜可以理解,毕竟一线大厂sp。但是骂楼主糊涂的大可不必,说什么会被社会毒打更是丢人。女朋友体制内生活有保障,读研女朋友还供着,都准备订婚了人家两情相悦,二线本地以后两口子日子美滋滋,哪轮到你一个一线城市房子都买不起的996清高计小将在这说人家傻😅
点赞 评论 收藏
分享
评论
1
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务