icode icode
首页
  • Android学习

    • 📁基础内容
    • 📺AndroidCore
    • 🎨Android-UI
    • 🏖️Components
    • 📊Fragment
    • 🔗网络操作
    • 🔏异步机制
    • 📦数据存储
    • 🗃️Gradle
  • 学习笔记

    • 『框架』笔记
    • 『Kotlin』笔记
    • 《Vue》笔记
    • 《Git》学习笔记
    • 『Bug踩坑记录』
  • ListView
  • RecyclerView
  • ViewPager
  • Java笔记

    • 🟠JavaSE
    • 🟢JavaWeb
    • 🔴JavaEE
    • ⚪JavaTopic
    • 🍳设计模式
  • 计算机基础

    • 📌计算机网络
    • 🔍数据结构
    • 📦数据库
    • 💻OS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 关于

    • 📫关于我
  • 收藏

    • 网站
    • 资源
    • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

iqqcode

保持对技术的探索实践与热爱
首页
  • Android学习

    • 📁基础内容
    • 📺AndroidCore
    • 🎨Android-UI
    • 🏖️Components
    • 📊Fragment
    • 🔗网络操作
    • 🔏异步机制
    • 📦数据存储
    • 🗃️Gradle
  • 学习笔记

    • 『框架』笔记
    • 『Kotlin』笔记
    • 《Vue》笔记
    • 《Git》学习笔记
    • 『Bug踩坑记录』
  • ListView
  • RecyclerView
  • ViewPager
  • Java笔记

    • 🟠JavaSE
    • 🟢JavaWeb
    • 🔴JavaEE
    • ⚪JavaTopic
    • 🍳设计模式
  • 计算机基础

    • 📌计算机网络
    • 🔍数据结构
    • 📦数据库
    • 💻OS
  • 技术文档
  • GitHub技巧
  • Nodejs
  • 博客搭建
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 关于

    • 📫关于我
  • 收藏

    • 网站
    • 资源
    • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • JavaSE

    • 面向对象

    • 常用API

    • 集合类

    • 多线程

    • JDK8新特性

    • IO流

    • JVM

      • 类加载系统
      • 运行时数据区及线程
      • 程序计数器
      • 虚拟机栈
      • 本地方法栈
      • 堆
      • 方法区
      • 对象实例化的内存布局与访问定位
      • 面试考点
      • 垃圾回收算法
      • 执行引擎
      • 垃圾回收相关概念
      • 垃圾回收算法
      • 垃圾回收器
      • GC日志分析
      • JVM内存区域与内存溢出异常
      • JVM垃圾回收器与内存分配策略
      • Java内存模型JMM详解
      • StringTable详解
      • 站在虚拟机栈的角度,用字节码来理解i++和++i
    • JavaWeb

    • JavaEE

    • JavaTopic

    • 设计模式

    • 计算机基础

    • Java后端
    • JavaSE
    • JVM
    iqqcode
    2021-06-17
    目录

    站在虚拟机栈的角度,用字节码来理解i++和++i

    # 面试官的拷问

    👨‍🦲面试官:“说一说i++跟++i的区别”

    😈我:“这个我懂!i++是先把i的值拿出来使用,然后再对i+1,++i是先对i+1,然后再去使用i”

    👨‍🦲面试官:你说你懂i++跟++i的区别,那你知道下面这段代码的运行结果吗?

    😈我:“以我零年的开发经验来看,它必然不会是10!”

    我开玩笑的(熊猫人)_熊猫人_开玩笑表情

    👨‍🦲面试官:你可真会开玩笑啊_可真会_开玩笑表情

    😈我:“哈哈…,开个玩笑,结果为0啦”

    👨‍🦲面试官:“你能说说为什么吗?”

    😈我:“因为j++这个表达式每次返回的都是0,所以最终结果就是0”

    👨‍🦲面试官:“小伙子不错,那你能从JVM的角度讲一讲为什么嘛?”

    😈我心想:这货明显是在搞事情啊,这么快就到JVM了?还好我有准备。

    想要解释清楚这个问题,我们就必须要了解JVM的内存区域划分,主要涉及到的是虚拟机栈,通过JVM字节码指令来做具体分析!

    # 暖场准备

    JVM的运行时数据区Rutime Data Area分为这5大块:

    虚拟机栈中又分为以下四块区域:

    • 局部变量表(Local Variables)

    • 操作数栈(Operand Stack)

    • 动态链接(Dynamic Linking)

    • 方法返回地址(Return Address)

    局部变量表Local Variables(局部变量数组或者本地变量表)。

    1. 局部变量表是一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。

      • 数据类型包括:基本数据类型、引用类型、返回值类型
    2. 线程私有数据,不存在线程安全问题

    3. 局部变量表的容量大小在编译期间被确定,在方法运行期间不会改变

    其中的局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

    局部变量表的最小存储单元为Slot(槽),其中64位长度的long和double类型的数据会占用2个Slot,其余的数据类型只占用1个。所以我们可以将局部变量表分为一个个的存储单元,每个存储单元有自己的下标位置,在对数据进行访问时可以直接通过下标来访问

    操作数栈对于数据的存储跟局部变量表是一样的,但是跟局部变量表不同的是,操作数栈对于数据的访问不是通过下标而是通过标准的栈操作来进行的(压入与弹出),之后在分析字节码指令时我们会很明显的感觉到这一点。另外还有,对于数据的计算是由CPU完成的,所以CPU在执行指令时每次会从操作数栈中弹出所需的操作数经过计算后再压入到操作数栈顶。

    # 开始表演

    以执行下面这段代码为例:

    这个过程如下所示

    在这里插入图片描述

    这两步完成了局部变量a的赋值,同理b的赋值也一样,a,b完成赋值后此时的状态如下图所示

    此时要执行a+b的运算了,所以首先要将需要的操作数加载到操作数栈,执行运算时再将操作数从栈中弹出,由CPU完成计算后再将结果压入到栈中,整个过程如下:

    到这里还没有完哦,还剩最后一步,需要将计算后的结果赋值给c,也就是要将操作数栈的数据弹出并赋值给局部变量表中的第三个槽位

    在这里插入图片描述

    OK,到这一步整个过程就完成了


    👨‍🦲面试官:“嗯,说的不错,但是你还是没解释为什么最开始的那个问题,为什么j=j++的结果会是0呢?”

    😈我:“面试官您好,要解释这个问题上面的知识都是基础,真正要说明白这个问题我们需要从字节码入手。”

    我们进入到这段代码编译好的.class文件目录下执行反编译操作:

    javap -c xxx.class

    得到其字节码如下:

      public void test1();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=3, args_size=1
             0: iconst_0
             1: istore_1
             2: iconst_0
             3: istore_2
             4: iload_2
             5: bipush        10
             7: if_icmpge     21
            10: iload_1
            11: iinc          1, 1
            14: istore_1
            15: iinc          2, 1
            18: goto          4
            21: getstatic     #2  
            24: iload_1
            25: invokevirtual #3 
            28: return
          LineNumberTable:
            line 18: 0
            line 19: 2
            line 20: 10
            line 19: 15
            line 22: 21
            line 23: 28
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                4      17     2     i   I
                0      29     0  this   Liqqcode/pcregister/problemsAdd;
                2      27     1     j   I
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33

    Code:
          0: iconst_0    // 将常数0压入到操作数栈顶
          1: istore_1    // 将操作数栈顶元素弹出并压入到局部变量表中1号槽位,也就是j=0
          2: iconst_0    // 将常数0压入到操作数栈顶
          3: istore_2     // 将操作数栈顶元素弹出并压入到局部变量表中2号槽位,也就是i=0
          4: iload_2     // 将2号槽位的元素压入操作数栈顶
          5: bipush        10    // 将常数10压入到操作数栈顶,此时操作数栈中有两个数(常数10,以及i)
          7: if_icmpge     21     // 比较操作数栈中的两个数,如果i>=10,跳转到第21行
          10: iload_1             // 将局部变量表中的1号槽位的元素压入到操作数栈顶,就是将j=0压入操作数栈顶
          11: iinc          1, 1 // 将局部变量表中的1号元素自增1,此时局部变量表中的j=1
    
          14: istore_1             // 将操作数栈顶的元素(此时栈顶元素为0)弹出并赋值给局部变量表中的1号    
          //槽位(一号槽位本来已经完成自增了,但是又被赋值成了0)
    
          15: iinc          2, 1 // 将局部变量表中的2号槽位的元素自增1,此时局部变量表中的2号元素值为1,也就是i=1
    
          18: goto          4     // 第一次循环结束,跳转到第四行继续循环
          21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
          24: iload_1
          25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
          28: return
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    操作数栈主要用来保存计算过程的中间结果,同时作为计算过程中 变量临时的存储空间。

    我们着重关注第10,11,14行字节码指令,用图表示如下:

    指令10: 将局部变量表中的1号槽位的元素压入操作数栈顶,即 j = 0 入栈

    指令11: 将局部变量表中的1号槽位的元素自增1,即 j = 1

    指令14: 将操作数栈顶元素(j = 0)出栈并赋给局部变量表1号槽位

    此时操作数栈顶 j  = 0,局部变量表中1号槽位的值本来自增了,变成了 j = 1.

    但是操作数栈没自增就把值赋给了局部变量表1号槽,此时 j = 0

    可以看到本来局部变量表中的j已经完成了自增(iinc指令是直接对局部变量进行自增),但是在进行赋值时是将操作数栈中的数据弹出,但是操作数栈的数据并没有经过计算,所以每次自增的结果都被覆盖了。

    最终结果就是0。


    我们平常说的i++是先拿去用,然后再自增,而++i是先自增再拿去用。这个到底怎么理解呢?如果站在JVM的层次来讲的话,应该这样说:

    1. i++是先被操作数栈拿去用了(先执行的load指令),然后再在局部变量表中完成了自增,但是操作数栈中还是自增前的值

    2. ++1是先在局部变量表中完成了自增(先执行innc指令),然后再被load进了操作数栈,所以操作数栈中保存的是自增后的值

    根本的区别就是innc和load指令先后执行的顺序问题。

    这就是它们的根本区别。


    # 拓展

    public static test2() {
        int i = 4;
        int b = i++;
        int a = ++i;
    }
    
    public void test2();
    Code:
           0: iconst_4
           1: istore_1
           2: iload_1
           3: iinc          1, 1
               6: istore_2
           7: iinc          1, 1
               10: iload_1
          11: istore_3
          12: return
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    安康是戴纳肯SVNAV那


    原文参考自:【面试官:你说你懂i++跟++i的区别,那你会做下面这道题吗】 (opens new window)

    编辑 (opens new window)
    上次更新: 2021/06/27, 10:49:09
    StringTable详解
    BootStrap快速入门

    ← StringTable详解 BootStrap快速入门→

    最近更新
    01
    匿名内部类
    10-08
    02
    函数式接口
    10-08
    03
    ARouter-Kotlin踩坑
    10-05
    更多文章>
    Theme by Vdoing | Copyright © 2021-2023 iqqcode | MIT License | 备案号-京ICP备2021028793号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×