解释 i = i++为什么等于本身的问题——JVM内存模型学习笔记

       前段时间在扫题的时遇到类似以下的例子:

public class Test {
	public static void main(String [] args) {
		
		int i = 1 ;
		//int j = i++;
		i = i++;//先赋值i= 1,i本身再自增变为2
		System.out.println(i);	
	}
}

       按照我们预期的结果,输出应该是2的,但实际的结果输出却是1,期间我也有问过一些同学和朋友,并且还在网上搜索相关推文进行解读,不过总觉得有一些方面没有解释清楚的,特别是结合机械指令画图进行描述时有点含糊(可能跟个人的理解有关,没有理解到点上),下面将结合JVM内存模型的虚拟机栈进行描述。

(一)JVM内存模型

       在解释上述代码前,我们可以先简单了解一下JVM内存模型(从此图解为JDK1.8版本)是怎样的。如下图:(画图为参考哔站视频:https://b23.tv/V35rV3n,该视频主要是讲解JVM调优思路,个人觉得讲得很好,非常推荐。

在这里插入图片描述

简单解释:
       元空间所有线程共享的数据区;(在jdk1.7之后,方法区称为元空间,并且属于本地内存部分,它包含元数据区和直接内存两个部分)
       Java虚拟机栈、本地方法栈和程序计数器线程隔离的数据区,即线程私有的数据区

(1)Java虚拟机栈

       Java虚拟机栈描述的是Java 方法执行的内存模型,每个线程创建时都会分配一个线程栈空间,而在线程栈空间中,由一个一个栈帧组成,它相当于Java虚拟机中所分配的一小块内存部分,其中每个方法被执行的时候都会同时创建一个栈帧,而栈帧的内部结构包括存储局部变量表、操作数栈、动态链接、方法出口等。

  1. 局部变量表:局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
  2. 操作数栈:用于运算的临时数据存储区,通过压栈和出栈来进行访问。
  3. 动态链接:将符号引用转换为直接引用的过程。
  4. 方法出口:上一个方法执行地址。

       以原文的代码为例,栈帧内存结构图如下所示:
在这里插入图片描述

       每执行一次本文中的代码,都会分配一个线程栈,并且该线程栈中由main方法的栈帧组成,假如main方法中存在嵌套调用,代码如下:

public class Test {

	public static void test(){
		System.out.println("测试");
	}
	
	public static void main(String [] args) {
		int i = 1 ;
		//int j = i++;
		i = i++;//先赋值i= 1,i本身再自增变为2
		System.out.println(i);	
		test();
	}
	
}

       则该线程栈的内存分布结构如下:
在这里插入图片描述
       此处会按照栈的先进后出(FILO)特点,首先将main方法的栈帧入栈,再将test方法的栈帧入栈,这也印证了先调用的方法后结束(内存回收),后调用的方法先结束(内存回收)的问题。

(二)反汇编字节码文件

       因为我们需要清楚Java代码在编译之后具体的执行逻辑是怎样的,可以利用JDK环境自带的javap命令进行反汇编,就能查看JVM底层中执行的机器指令代码(否则直接打开的class文件,里面全部都是二进制编码,可读性比较差),分析i = i++的问题。
首先找到当前类的class文件,如图所示:
在这里插入图片描述
       可以通过开发工具打开项目本地路径,class文件在target/classes目录下可以找到,然后打开命令窗口,输入以下命令并且回车,就能看到新生成一个txt文件:

javap -c Test.class > Test.txt

截图如下:
在这里插入图片描述
在这里插入图片描述
       此时打开txt文件之后,反汇编代码如下:

Compiled from "Test.java"
public class com.testProject.test.Test {
  public com.testProject.test.Test();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

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

       此时我们只需要关注main方法里面的指令就行,根据学习视频的提示可以推测出以下指令含义:

iconst_1:将int类型的值压入  操作数栈  的第2个位置(第一个位置(即iconst_0)通常为保存this指针对象)。
istore_1:将int类型的值从  操作数栈  中出栈,存入  局部变量表  中的局部变量1中。
iload_1:将局部变量1int类型的值装载到  操作数栈  中。
iinc  1,1:局部变量自增指令,第一个数字代表是局部变量表中的  局部变量序号  ,第二个数字代表要  自增值  ,整个指令的含义为:局部变量1自增1。(注:该自增操作  仅修改局部变量表  中的局部变量的值,并不是在操作数栈中进行自增)
getstatic     #16 :获取指定地址的打印流对象
invokevirtual   #22 :执行打印流对象的printf方法,将指定地址的值打印到控制台中
(后面两个命令不是重点,此处解释仅为个人理解。)

       通过对比java代码和机械指令,我们也很容易看出他们的大概关系,因此就可以画出以下执行流程图:
在这里插入图片描述
(1)iconst_1。首先将“1”压入操作数栈中(此处其实是先给1分配一块内存,然后再压入操作数栈中,省略其中的步骤),如下图所示:
在这里插入图片描述
(2)istore_1。将“1”从操作数栈中进行出栈,然后存入局部变量表中的局部变量i(此处其实是先在局部变量表中分配局部变量i的内存,然后再将值存入i中,省略其中的步骤),如下图:
在这里插入图片描述

(3)iload_1 。将局部变量i中int类型的值装载到操作数栈中,如下图:
在这里插入图片描述
(4)iinc 1, 1。对局部变量i进行自增操作,如下图:
在这里插入图片描述
(5)istore_1。将操作数栈中的值出栈,然后存入到局部变量i中,如下图:
在这里插入图片描述
       执行到这里,想必大家能看出来是什么问题,因为此时操作数栈中的值“1”将局部变量i的值"2"覆盖了,导致最终i输出还是1。

(三)比较

       作为对比,我们再看一下以下代码的反汇编指令:

public class Test {
	public static void main(String [] args) {
		int i = 1 ;
		int j = i++;
		//i = i++;
		System.out.println(i);	
	}
}

       反汇编后的指令代码为:

Compiled from "Test.java"
public class com.testProject.test.Test {
  public com.testProject.test.Test();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return

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

       可以看到,前面四个指令操作都是相同的,直到第五个操作istore_2,如下图所示:
在这里插入图片描述
       此时从操作数栈中出栈的值,会存入到新的局部变量j中,而没有覆盖i,所以对i没有任何影响。因此,针对i++和++i相关的运算,例如i = i++ + ++i等之类的运算,我们都可以通过反汇编class文件和画图的方式,了解底层指令的执行形式,验证运算结果的正确性。

(四)结语

       本文是参考一个哔站视频的思路进行编写,再次将原文链接保留出来:https://b23.tv/V35rV3n,可能行文思路没有很严谨,有很多关键点没有讲述清楚,可以去到链接中的视频进行学习,会更清楚一些。
       另外,本文为学习中汇总的内容,可能会跟其他人的讲述有偏差,或者是理解出错,各位读者可以提出批评指正,谢谢你们。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值