从字节码层面解析Java语言--i与i--的区别
概述:
众所周知,--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为例
(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中引用的常量。
附录:
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/
转载自:https://juejin.cn/post/7155503985695129613