当前位置: 首页 > news >正文

【JVM系列】深入理解Java虚拟机(JVM)的核心技术 :从程序计数器到栈帧结构(二、Java虚拟机栈探秘)

文章目录

  • 【JVM系列】深入理解Java虚拟机(JVM)的核心技术 :从程序计数器到栈帧结构(二、Java虚拟机栈探秘)
    • 程序计数器
    • 1. 基本概念
    • 2. 栈帧内部结构原理分析
      • 2.1 基本概念
      • 2.2 局部变量表
      • 2.3 Slot(变量槽--index)
      • 2.4 jclasslib分析字节码
      • 2.5 变量槽的复用
      • 2.6 局部变量表(的作用)总结
      • 2.7 操作数栈分析
      • 2.8 ++i与i++的底层原理
      • 2.9 栈溢出(StackOverflowError)
      • 2.10 动态链接
      • 2.11 方法出口

【JVM系列】深入理解Java虚拟机(JVM)的核心技术 :从程序计数器到栈帧结构(二、Java虚拟机栈探秘)

JVM内存结构
image-20240914215007240

程序计数器

  1. 程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行字节码的行号指示器,指向下一个将要执行的指令代码,由执行引擎来读取下一条指令。更确切的说,一个线程的执行,是通过字节码解释器改变当前线程的计数器的值,来获取下一条需要执行的字节码指令,从而确保线程的正确执行。

  2. 为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。

  3. 程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。

程序计数器作用:记录下一个 jvm 指令的执行地址

image-20240914215143942

1. 基本概念

1.Java 虚拟机栈也是线程私有的,它的⽣命周期和线程相同,描述的是 Java⽅法执⾏的内存模型,每次⽅法调⽤的数据都是通过栈传递的。

2.虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧[1](Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息,每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

相关代码:

public static void main(String[] args) {a();
}public static void a() {b();
}public static void b() {c();
}public static void c() {
}

方法压栈与出栈:

image-20240914221850028

栈会遵循先进后出原则 每个方法会创建一个栈帧,在栈帧中存放该方法对应的局部变量表

Idea 调试分析栈帧:

image-20240914222403724

2. 栈帧内部结构原理分析

2.1 基本概念

在每一个方法对应的栈帧中都会有自己独立的:栈帧包含方法的所有信息

  • 局部变量表:存放当前方法对应的局部变量;

  • 操作数栈:或表达式栈;

  • 动态链接:或指向运行时常量池的方法引用;

  • 方法出口:或方法正常退出或者异常退出的定义;

image-20240916225040559

2.2 局部变量表

局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

public class Test02 {public static void main(String[] args) {String str = "zhaoli";int j = 20;double d = 66.66;boolean b = true;}public int a() {int i = 10;int j = 20;int k = (i + j) * 2;return k;}public static int b() {int ii = 10;int jj = 20;int kk = (ii + jj) * 2;return kk;}
}
  1. 先将这个.java文件编译成.class文件

  2. cmd窗口 cd.class文件所在目录

  3. 使用 javap -v Test02 命令得到汇编代码

main()对应的局部变量表

image-20240914224335840

方法传递的参数也会在局部变量表中存储

a()对应的局部变量表

image-20240914224806730

如果我们的方法是实例方法或者是构造方法(函数),则jvm默认会在局部变量表中创建 一个当前对象变量名称为 this, 存入在我们当前方法对应的局部方法表中第0个位置 这样我们就可以在实例方法中使用 this,相当于默认添加了局部变量a(Test02 this);静态方法不会

b()对应的局部变量表

image-20240914225224069

2.3 Slot(变量槽–index)

局部变量表最基本的存储单元就是变量槽。

在局部变量表里,32位以内的类型只占用一个slot (包括returnAddress类型),64位的类型(longdouble)占用两个slot

注意:shortbyteboolean等数据也占用一个变量槽,因为jvm会在存储时将上述变量转为int类型(变量槽是最基本存储单元,无法分割,只能整个使用)。

public static void main(String[] args) {String str = "zhaoli";int j = 20;double d = 66.66;boolean b = true;
}

image-20240914224335840

JVM会为局部变量表中每一个变量分配变量槽,并记录其的存储位置,比如main函数方法传递了String [] args 数组,变量args就存储在index0的变量槽中,变量d因为为64位,需要占用两个变量槽(34),变量b因为D占用了两个变量槽,所以直接从index5处开始存储。

image-20240916154251373

2.4 jclasslib分析字节码

  1. idea 安装使用

    • 打开idea 中的 settings > plugins 搜索 jclasslib 插件 进行安装 重启生效
    • 重启后点击view > 选择show bytecode with jclasslib
  2. jclasslib git地址:https://github.com/ingokegel/jclasslib

image-20240916154431109

image-20240916154458661

不赋值(在代码块中赋值)的变量,在局部变量表中不显示,但局部变量表仍会为其预留位置。

public void test() {int a;int b;int c;
}

image-20240916154741062

2.5 变量槽的复用

public static void test() {int a = 0;{int b=30;System.out.println("zhaoli");}int c = 0;
}
image-20240916155203575

此处原因就是JVM对变量槽有一个复用性为,当变量b超出其作用域后不再生效,所以变量c直接占据了b的位置,所以局部变量表中会少一个位置。

2.6 局部变量表(的作用)总结

  1. 局部变量表只对已确定一定有值的变量和方法参数进行记录,在程序执行中得以直接使用;
  2. 存放当前方法入参和局部变量槽0开始计算 longdouble 占用2个槽位;
  3. 如果我们的方法是为实例方法构造函数,则在局部变量表中槽0的位置默认存放当前对象,变量名是为 this;
  4. 如果代码块结束(作用域结束) ,jvm会对变量槽有一个复用的行为,以便于节省空间。

2.7 操作数栈分析

public int compute() {int a = 10;int b = 20;int c = (a + b) * 10;return c;
}

iconst_0:将int类型的0值压入操作数栈 …

iconst_5:将int类型的5值压入操作数栈

istore_1: 弹出操作数栈顶的值赋给局部变量表下标为1的变量

iload_1: 将局部变量表下标为1的位置存储的值压入操作数栈

iinc 1 by 1:取局部变量表下标为1的位置存储的值加上1

istore_1:弹出操作数栈顶的值赋给局部变量表下标为1的变量

底层汇编代码:

 0: bipush        10  ##  将一个8位带符号整数压入栈 102: istore_1       局部变 量表中槽1的位置存入10;3: bipush        20 ##  将一个8位带符号整数压入栈 205: istore_2       局部变量表中槽2的位置存入20;6: iload_1        从局部变量表中槽1的位置 获取 变量a=10;7: iload_2        从局部变量表中槽2的位置 获取 变量b=20;8: iadd           iadd 执行int类型的加法 10+20 9: bipush         10 ## 将一个8位带符号整数压入栈 10
11: imul           imul 执行int类型的乘法30*10
12: istore_3        局部变量表中槽3的位置存入300 c=300;
13: iload_3         最后返回局部变量表中槽3的位置
14: ireturn

2.8 ++i与i++的底层原理

i++是先赋值,然后再自增;++i是先自增,后赋值。

i++是直接在局部变量表加的,没有在操作数栈里运算

public static void c(){int i=0;int z=i++;System.out.println(z);
}public static void d(){int i=0;int z=++i;System.out.println(z);
}
## i++0: iconst_01: istore_02: iload_03: iinc          0, 16: istore_17: getstatic     #410: iload_1Start  Length  Slot  Name   Signature2      13     0     i   I7       8     1     z   I## ++i0: iconst_01: istore_02: iinc          0, 15: iload_06: istore_17: getstatic     #4      10: iload_111: invokevirtual #514: returnLineNumberTable:line 37: 0line 38: 2line 39: 7line 40: 14LocalVariableTable:Start  Length  Slot  Name   Signature2      13     0     i   I7       8     1     z   Ib

不同点就在 2: iload_0 3: iinc 0, 1 2: iinc 0, 1 5: iload_0

i++++i底层区别

  • i++ 先将局部变量表中的值压入(放入)到操作数栈中,在直接对局部变量中做+1操作。

  • ++i 先将局部变量表中的值做+1的操作,在将局部变量表中加1之后的结果压入到操作数栈中。

2.9 栈溢出(StackOverflowError)

StackOverflowError代表的是,当栈深度超过虚拟机分配给线程的栈大小时就会出现此error

public class StackOverFlow {private int i;public void plus() {i++;plus();}/*** 设置 -Xss128k* @param args*/public static void main(String[] args) {StackOverFlow stackOverFlow = new StackOverFlow();try {stackOverFlow.plus();} catch (Error e) {System.out.println("Error:stack length:" + stackOverFlow.i);e.printStackTrace();}}
}

image-20240916224154635

在栈空间内存中是否会发生线程安全问题呢?

在讨论栈空间内存与线程安全问题时,我们需要理解栈空间的主要作用和线程安全的基本概念。

  1. 栈空间:在程序运行时,栈空间主要用于存放函数调用时的局部变量、函数参数、返回地址等临时数据。当一个函数或方法执行完毕后,这些数据就会被自动清理出栈。

  2. 线程安全:线程安全是指在一个多线程环境中,多个线程并发访问共享资源时,不会导致数据的不一致或其他错误状态。为了保证线程安全,通常需要使用同步机制(如互斥锁、信号量等)来控制对共享资源的访问。

在栈空间内存中本身是不容易发生线程安全问题的,因为每个线程都有自己独立的栈空间,不同线程之间不能直接访问对方栈中的数据。这意味着,如果某个变量是在栈上分配的,并且仅由创建它的线程所访问,那么它就不会有线程安全问题。

然而,在实际编程过程中,可能会有一些情况导致间接的线程安全问题,例如:

  • 如果一个线程在其栈上定义了一个指向堆上共享资源的指针或引用,并且将这个指针或引用传递给了其他线程,那么这些线程就可能并发地修改该共享资源,从而引发线程安全问题。
  • 如果栈上的数据结构(如数组)被用来引用或包含堆上的共享对象,并且这些对象没有适当的同步保护,则可能会出现线程安全问题。

总之,栈空间本身是线程隔离的,但如果涉及到共享数据(尤其是堆上的数据),则需要采取措施来确保线程安全。

2.10 动态链接

动态链接:每个栈帧都保存了一个可以指向当前方法所在类的运行时常量池;

目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,之后就能直接调用对应方法,这就是动态链接。

2.11 方法出口

方法出口定义了方法正常退出或者异常退出的方式。每种退出方式都会导致栈帧被销毁,同时伴随着可能的控制流转移、栈顶元素的弹出以及局部变量表的清空。

  1. 正常退出:当方法执行完最后一条指令或者执行到诸如return之类的返回指令时,方法即正常退出。此时,方法返回值(如果是有的话)将被压入调用者栈帧的操作数栈中,然后控制权返回给上一层方法。

  2. 异常退出:如果方法执行过程中出现了异常,并且这个异常没有在方法内部被捕获处理,那么方法就会异常退出。此时,虚拟机会寻找一个合适的异常处理器来处理这个异常。如果找不到合适的处理器,那么这个异常最终会上抛给更高层次的调用者,直到被处理或者导致程序终止。

无论哪种退出方式,一旦方法退出,其对应的栈帧就会从虚拟机栈中弹出,为新的方法调用腾出空间。这是由于栈的特性决定的——先进后出(LIFO),即最后进入的方法最先退出。这样的机制保证了方法调用的正确性和内存使用的高效性。


http://www.mrgr.cn/news/46833.html

相关文章:

  • CSS的小知识
  • Springboot + vue 图书管理系统
  • Three.js 性能优化:打造流畅高效的3D应用
  • lerna使用指南
  • 【深度学习】布匹寻边:抓边误差小于3px【附完整链接】
  • 【1】Word:邀请函
  • 读数据工程之道:设计和构建健壮的数据系统04数据工程生命周期(下)
  • <<迷雾>> 第10章 用机器做一连串的加法(4)--带传输门和寄存器的加法器 示例电路
  • C# 结构体(Struct)
  • 微分方程(Blanchard Differential Equations 4th)中文版Exercise 1.5
  • 进阶功法:SQL 优化指南
  • USB UVC7 -- XU
  • 基于springboot vue在线学籍管理系统设计与实现
  • 【hot100-java】N 皇后
  • PMP--冲刺题--解题--71-80
  • 【C++差分数组】P1672何时运输的饲料
  • Golang | Leetcode Golang题解之第468题验证IP地址
  • 深入解析RBAC模型的数据库设计方案
  • PGMP-05相关方
  • IDEA调试模式下,单步执行某修改方法后,数据库内容没有更新,同时也无法手动修改对应数据
  • C语言 | Leetcode C语言题解之第468题验证IP地址
  • IDEA必装的插件:Spring Boot Helper的使用与功能特点
  • 冷热数据分离
  • Python中的列表:全面解析与应用
  • 【C语言】值传递和指针传递
  • Excel重新踩坑1:加密保护工作簿、编辑保护工作簿、编辑保护工作表、允许编辑区域;填充柄;同时编辑多个单元格为同一个值