<JVM上篇:内存与垃圾回收篇>03-程序计数器 | 虚拟机栈(三)

简介: <JVM上篇:内存与垃圾回收篇>03-程序计数器 | 虚拟机栈

4.3.1. 关于 Slot 的理解


局部变量表,最基本的存储单元是 Slot(变量槽)


参数值的存放总是在局部变量数组的 index 0 开始,到数组长度-1 的索引结束。


局部变量表中存放编译期可知的各种基本数据类型(8 种),引用类型(reference),returnAddress 类型的变量。


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


byte、short、char 在存储前被转换为 int,boolean 也被转换为 int,0 表示 false,非 0 表示 true。

long和double则占据两个Slot

JVM 会为局部变量表中的每一个 Slot 都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值


当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个 slot 上


如果需要访问局部变量表中一个 64bit 的局部变量值时,只需要使用前一个索引即可。(比如:访问 long 或 double 类型变量)


如果当前帧是由构造方法或者实例方法创建的,那么该对象引用 this 将会存放在 index 为 0 的 slot 处,其余的参数按照参数表顺序继续排列。


229d2c5ec47176808059829e278deffd.png


方法与局部变量表对照图:


665c1e5b9f08ae4248c241a4f1ffa33e.png


4.3.2. Slot 的重复利用


栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。


Slot重复利用示意图:


301e1fb116fd07705b804205b89398d4.png


4.3.3. 静态变量与局部变量的对比


参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。


我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。


和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。

public void test(){
    int i;
    System. out. println(i);//System.out.println(num);//错误信息:Variable 'num' might not have been initialized
}

这样的代码是错误的,没有赋值不能够使用。


**补充:**变量的分类:


按照数据类型分:① 基本数据类型 ② 引用数据类型

按照在类中声明的位置分:

成员变量:在使用前,都经历过默认初始化赋值.

类变量(静态变量): linking的prepare阶段:给类变量默认赋值 —> initial阶段:给类变量显式赋值即静态代码块赋值

实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值

局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过


4.3.4. 补充说明


在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。


局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。


4.4. 操作数栈(Operand Stack)


每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的 操作数栈,也可以称之为表达式栈(Expression Stack)


操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)


某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈

比如:执行复制、交换、求和等操作

2a6db6fbdcbf204c7df774272cbcf615.png

代码举例

public void testAddOperation(){
    byte i = 15;
    int j = 8;
    int k = i + j;
}

字节码指令信息

public void testAddOperation();
    Code:
    0: bipush 15
    2: istore_1
    3: bipush 8
    5: istore_2
    6:iload_1
    7:iload_2
    8:iadd
    9:istore_3
    10:return

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


操作数栈就是 JVM 执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。


每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的 Code 属性中,为 max_stack 的值。


栈中的任何一个元素都是可以任意的 Java 数据类型


32bit 的类型占用一个栈单位深度

64bit 的类型占用两个栈单位深度

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问


如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新 PC 寄存器中下一条需要执行的字节码指令。


操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。


另外,我们说 Java 虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。


**备注:**操作数栈和局部变量表的底层都是数组,所以对于double和long类型数据需要两个单位存储.


4.5. 代码追踪

public void testAddOperation() {
    byte i = 15;
    int j = 8;
    int k = i + j;
}


使用 javap 命令反编译 class 文件: javap -v 类名.class

public void testAddoperation();     
  Code: 
     0 bipush 15
     2 istore_1
     3 bipush 8
     5 istore_2
     6 iload_1
     7 iload_2
     8 iadd
     9 istore_3
    10 return

e9261231648baa24763fc3793d784f20.png

914e23e832f563ae376c15e3da4769b4.png

3055e8553a6d4384ea44d4b29043b0a6.png

49026433c7ec6e427f51de42600bb720.png

c35e3ce1222e0f107c5641faa8082e9f.png

acbaba8894ebb7bb64f45d0ec5840282.png

4f2281d29c6db52a247bec4d2dce0910.png

92fa27284aa5e058b5e7c86dc44267ac.png

涉及操作数栈的字节码指令执行分析:

5f41973d7536c3b9d7f57dc4c14ca699.png


  • istore 指令会导致出栈 并且写入局部变量表 ipush:放入操作数栈
  • istore和istore一样,只不过istore只有0到3(其实是四个不同的指令,操作数隐式指定),再往后就得用istore了,因为还需要显式指出槽位,所以要占两个字节.
相关文章
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
1033 55
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
1040 6
|
8月前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
557 5
|
8月前
|
存储 缓存 算法
深入理解JVM《JVM内存区域详解 - 世界的基石》
Java代码从编译到执行需经javac编译为.class字节码,再由JVM加载运行。JVM内存分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)区域,其中堆是GC主战场,方法区在JDK 8+演变为使用本地内存的元空间,直接内存则用于提升NIO性能,但可能引发OOM。
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
771 29
JVM简介—1.Java内存区域
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
存储 设计模式 监控
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
398 0
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
|
11月前
|
存储
阿里云轻量应用服务器收费标准价格表:200Mbps带宽、CPU内存及存储配置详解
阿里云香港轻量应用服务器,200Mbps带宽,免备案,支持多IP及国际线路,月租25元起,年付享8.5折优惠,适用于网站、应用等多种场景。
3291 0
|
11月前
|
存储 缓存 NoSQL
内存管理基础:数据结构的存储方式
数据结构在内存中的存储方式主要包括连续存储、链式存储、索引存储和散列存储。连续存储如数组,数据元素按顺序连续存放,访问速度快但扩展性差;链式存储如链表,通过指针连接分散的节点,便于插入删除但访问效率低;索引存储通过索引表提高查找效率,常用于数据库系统;散列存储如哈希表,通过哈希函数实现快速存取,但需处理冲突。不同场景下应根据访问模式、数据规模和操作频率选择合适的存储结构,甚至结合多种方式以达到最优性能。掌握这些存储机制是构建高效程序和理解高级数据结构的基础。
1080 1