基于源码剖析:深度解读JVM底层运行机制

每日禅语

佛说,给你修路的,是你自己;埋葬你的,也是你自己;帮助你的,是你自己;毁灭你的,也是你自己;成就你的,自然还是你自己。所以佛说:自作自受,自性自度!

文章背景

在我们Java Coder编写java代码的时候,通过编译器点击运行,然后就可以输出想要的结果,但是如果你仔细询问这段代码是如何通过java代码,变成.class文件,再通过JVM执行输出结果的时候,大部分人都不能很清楚的说出运行机制和运行原理。或者对于大部分面试者来说,JVM了解的都是JVM八股文信息,但是你要深究为什么需要这些区域,这些区域如果关联起来运行的,大部分人都难以说出其重点。本文就是以此为基础,通过源码一步一步分析程序的底层运行原理。

通过一道面试题分析底层:输出的结构是多少?

public class Client {
    public static void main(String[] args) {
        int count = 0;
        for (int i = 0 ; i <10 ;i ++){
            count = count++;
            System.out.println("count=" + count);
        }
    }
}

大部分人的第一印象答案:输出1-9,但是如果你让程序运行以后你会发现,结果和你想的大相径庭,竟然是输出的10个0,为什么结果和预期的差距这么大。后面的内容将会根据这段代码分析JVM的内存区域模型,从而让你知其然而知其所以然。

Java文件编译成class文件以后如何从磁盘读入内存中

详细讲解

Java字节码详细解析

javap -c .\Demo.class

public static void main(java.lang.String[]);
Code:
   0: iconst_0                 // 将常量 0 压入操作数栈,栈顶是 0
   1: istore_1                 // 将栈顶的 0 存储到局部变量 1 (即变量 count)
   2: iconst_0                 // 将常量 0 压入操作数栈,栈顶是 0
   3: istore_2                 // 将栈顶的 0 存储到局部变量 2 (即变量 i)
   4: iload_2                 // 加载局部变量 2 (即 i) 到操作数栈
   5: bipush        10         // 将常量 10 压入操作数栈
   7: if_icmpge     33         // 比较栈顶两个整数 (i和 10),如果 i >= 10,跳转到字节码地址 33 (即结束循环)
  10: iload_1                 // 加载局部变量 1 (即 count) 到操作数栈
  11: iinc          1, 1      // 将局部变量 1 (count) 增加 1
  14: istore_1                 // 将栈顶的值 (更新后的 count) 存储回局部变量 1
  15: getstatic     #7        // 获取静态字段 System.out (即输出流)
  18: iload_1                 // 加载局部变量 1 (即 count) 到操作数栈
  19: invokedynamic #13,  0   // 使用动态方法调用输出 (这里是用 makeConcatWithConstants 动态拼接字符串)
  24: invokevirtual #17       // 调用 PrintStream 的 println 方法,将拼接的字符串打印出来
  27: iinc          2, 1      // 将局部变量 2 (i) 增加 1
  30: goto          4         // 跳转回到字节码地址 4,继续循环
  33: return                  // 返回,结束程序

代码片段2:

public class Client {
    public static void main(String[] args) {
        int count = 0;
        for (int i = 0 ; i <10 ;i ++){
            count = ++count;
            System.out.println("count=" + count);
        }
    }
}

  public static void main(java.lang.String[]);
    Code:
       0: iconst_0
       1: istore_1
       2: iconst_0
       3: istore_2
       4: iload_2
       5: bipush        10
       7: if_icmpge     33
      10: iinc          1, 1
      13: iload_1
      14: istore_1
      15: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      18: iload_1
      19: invokedynamic #13,  0             // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
      24: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: iinc          2, 1
      30: goto          4
      33: return
}

count++的字节码操作:

  10: iload_1                 // 加载局部变量 1 (即 count) 到操作数栈
  11: iinc          1, 1      // 将局部变量 1 (count) 增加 1
  14: istore_1                 // 将栈顶的值 (更新后的 count) 存储回局部变量 1

总结:

先将局部变量表位置为1的count的值为0加载到操作数栈

再将局部变量1中的count + 1

再将操作数栈的值为1又更新到局部变量表1位置,虽然第二步已经将值加1,但是此时又给覆盖了,所以值始终为0

++count的字节码操作:

iinc 1, 1      // 将局部变量1的值加1
iload_1        // 将局部变量1的值加载到操作数栈
istore_1       // 将操作数栈顶的值存储到局部变量1

总结:

将局部变量表位置为1的count的值加1

再将局部变量表位置为1的count的值加载到操作数栈

再将操作数栈的值更新到局部变量表1位置,此时值就变为了1

操作数栈的作用:操作数栈的主要作用是提供一个地方来暂时存储数据,供执行各种指令时使用。它支持多种操作,如加载数据、执行算术运算、比较数据等。

局部变量表:用于存储方法的局部变量及一些相关信息。它在方法执行期间提供对局部变量的访问,并与操作数栈一起支持字节码的执行

局部变量表的数据结构

public class LocalVarExample {
    public LocalVarExample() {
    }

    public void testMethod(int a, long b) {
        int c = a + 1;
        float d = (float)c * 2.5F;
    }
}
局部变量表的数据结构
1.线性数组
  • 数组结构
    局部变量表以数组的形式实现,数组的每个元素称为一个 Slot(槽位)
    • 每个槽位固定为 32 位(4 字节)。
    • 一个槽位可以存储:
      • 一个 32 位的基本数据类型(如 intfloat 等)。
      • 一个引用类型(如对象引用)。
      • 一部分特殊数据(如 returnAddress,用于 jsrret 指令)。
    • 64 位数据类型(longdouble)会占用两个连续的槽位
2. 索引定位
  • 每个局部变量通过索引(Index)定位。
    • 索引从 0 开始。
    • 方法的第一个参数存储在索引为 0 的槽位,后续参数依次存储。
    • 方法参数结束后,局部变量存储在后续槽位。
3. 数据类型和槽位

局部变量表中没有显式记录变量的类型,但类型信息隐含在字节码指令中:

  • 例如,iload_1 表示从索引 1 的槽位加载一个 int 类型变量。
局部变量表的内存分配
  1. 初始化

    • 局部变量表的大小在方法调用时确定,大小由方法的 max_locals 指定。
    • max_locals 的值由编译器在编译时计算,表示方法所需的局部变量槽位的总数。
  2. 参数分配

    • 静态方法:
      • 方法参数按顺序分配到槽位。
    • 实例方法:
      • 索引 0 存储 this 引用,其他参数按顺序分配。
  3. 局部变量分配

    • 方法体中声明的局部变量在调用时分配到空闲的槽位。
      计算局部变量表的大小

局部变量表的大小取决于:

  1. 方法的参数
  2. 方法体内声明的局部变量
  3. 变量类型的大小

规则

  • 每个局部变量占用一个或多个 Slot
    • 32 位变量(int, float, reference, returnAddress):占用 1 个 Slot。
    • 64 位变量(long, double):占用 2 个连续的 Slot。
  • 静态方法不包含 this 引用。
  • 非静态方法的第一个 Slot 用于存储 this 引用。
局部变量表的操作
加载和存储

局部变量表与操作数栈之间通过加载(load)和存储(store)指令交互:

  • 加载指令 (如 iload, fload, aload):
    • 从局部变量表加载数据到操作数栈。
  • 存储指令 (如 istore, fstore, astore):
    • 从操作数栈存储数据到局部变量表。
2. 类型相关性
  • 加载和存储指令类型敏感。例如:
    • iload_1 表示从槽位 1 加载 int 类型。
    • dload_2 表示从槽位 2 和 3 加载 double 类型。
相关推荐
杨荧1 小时前
【开源免费】基于Vue和SpringBoot的网上商城系统(附论文)
前端·javascript·jvm·vue.js·spring boot·spring cloud·开源
编程乐学4 小时前
网络资源模板--Android studio 实现的校园座位预约App
jvm·oracle·android studio
叶子2024225 小时前
labelme下载
java·jvm·算法
xiaohao_g8 小时前
JAVA八股文-序列化和反序列化
java·开发语言·jvm
Java 第一深情8 小时前
面试题解,JVM的运行时数据区
jvm·java面试
CoderLi_17 小时前
Java 类加载机制
java·jvm·类加载
葡萄架子20 小时前
进程、线程和协程是什么,以及他们之间的区别
java·linux·jvm
工业甲酰苯胺20 小时前
JVM实战—JVM垃圾回收的算法和全流程
jvm·算法·linq
2401_8532757320 小时前
InnoDB存储引擎对MVCC的实现
jvm·数据库·oracle