Java虚拟机(六):Java虚拟机栈-栈帧

简介: Java虚拟机栈中栈帧的具体说明

Ⅰ. 栈帧内部结构

每个栈帧存储着:1.局部变量表(Local Variables)、2.操作数栈(Operand Stack)、3.动态链接(Dynamic Linking)、4.方法返回地址(Return Address)、5.一些附加信息

Ⅱ. 局部变量表

一、局部变量表基本理解

1.局部变量表也被称为局部变量数组或本地方法表

2.定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress

3.由于局部变量表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题

4.局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的

5.方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间。

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

查看Java虚拟机栈的栈帧的局部变量表

方法一:使用javap -v xxx.class查看

方法二:使用IDEA插件jclasslib查看

二、局部变量表中变量槽slot

局部变量表中变量槽slot的理解

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

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

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

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

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

long和double则占据两个slot

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

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

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

8.如果当前帧是由构造方法或者实例方法创建的(意思是当前帧所对应的方法是构造器方法或者是普通的实例方法),那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序排列。

9.静态方法中不能引用this,是因为静态方法所对应的栈帧当中的局部变量表中不存在this

public class LocalVariablesTest {

    private int count = 1;
    
    public static void testStatic(){
        //编译错误,因为this变量不存在与当前方法的局部变量表中!!!
        System.out.println(this.count);
    }
}

slot的重复利用

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

private void test2() {
        int a = 0;
        {
            int b = 0;
            b = a + 1;
        }
        //变量c使用之前以及经销毁的变量b占据的slot位置
        int c = a + 1;
    }

上述代码对应的栈帧中局部变量表中一共有多少个slot,或者说局部变量表的长度是几?

答案是3:this占0号、a单独占1个槽号、c重复使用了b的槽号

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

变量的分类:

  • 按照数据类型分:

    • ①基本数据类型
    • ②引用数据类型
  • 按照在类中声明的位置分:

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

      • static修饰:类变量:类加载链接的准备preparation阶段给类变量默认赋0值--->>初始化阶段initialization给类变量显式赋值即静态代码块赋值
      • 不被static修饰:实例变量:随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
    • ②局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过

Ⅲ. 操作数栈

1.每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈

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

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

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

操作数栈的特点

  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
  • 操作数栈就是JVM执行引擎的一个工作区,当一个方法开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就定义好了,保存在方法的code属性中,为max_stack的值。
  • 栈中的任何一个元素都是可以任意的Java数据类型

    • 32bit的类型占用一个栈单位深度
    • 64bit的类型占用两个栈深度单位
  • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈push和出栈pop操作来完成一次数据访问
  • 如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令
  • 操作数栈中的元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类验证阶段的数据流分析阶段要再次验证
  • 另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈

操作数栈具体演示

下面我们以此代码进行演示

①15入栈、②存储15,15进入局部变量表

③压入8、④8出栈,存储8进入局部变量表

⑤从局部变量表中把索引为1和2的是数据取出来,放到操作数栈、⑥iadd相加操作

⑦iadd操作结果23出栈并且操作数栈清空、⑧将23存储在局部变量表索引为3的位置上istore_3

栈顶缓存技术TOS(Top-of-Stack Cashing)

  • 基于栈式架构的虚拟机所使用的零地址指令(即不考虑地址,单纯入栈出栈)更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数
  • 由于操作数栈是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度,为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

Ⅳ. 动态链接

1.运行时常量池位于方法区(注意: JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池)

为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别

2.每一个栈帧内部都包含一个指向运行时常量池Constant pool或该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令

3.在Java源文件被编译成字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class字节码文件(javap反编译查看)的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用(#)最终转换为调用方法的直接引用。

方法的调用

在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关

  • 静态链接
    当一个 字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接
  • 动态链接
    如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接

对应的方法的绑定机制为:早起绑定(Early Binding)和晚期绑定(Late Bingding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次

  • 早期绑定
    早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用
  • 晚期绑定
    如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定

随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装,集成和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式
Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法

虚方法和非虚方法

非虚方法

  • 如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法
  • 静态方法、私有方法、final方法、实例构造器(实例已经确定,this()表示本类的构造器)、父类方法(super调用)都是非虚方法

其他所有体现多态特性的方法称为虚方法:子类对象的多态性使用前提:①类的继承关系(父类的声明)、②方法的重写(子类的实现)

JVM虚拟机中的方法调用指令

普通调用指令:
1.invokestatic:调用静态方法,解析阶段确定唯一方法版本
2.invokespecia:调用方法、私有及父类方法,解析阶段确定唯一方法版本
3.invokevirtual:调用所有虚方法
4.invokeinterface:调用接口方法
5.动态调用指令(invokedynamic,Java7新增):动态解析出需要调用的方法,然后执行 :

  • JVM字节码指令集一直比较稳定,一直到Java7才增加了一个invokedynamic指令,这是Java为了实现【动态类型语言】支持而做的一种改进
  • 但是java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令.直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接生成方式
  • Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本

其中invokestatic指令和invokespecial指令调用的方法称为非虚方法

其中invokevirtual(final修饰的除外,JVM会把final方法调用也归为invokevirtual指令,但要注意final方法调用不是虚方法)、invokeinterface指令调用的方法称称为虚方法

动态语言和静态语言

  • 动态类型语言和静态类型语言两者的却别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言
  • 直白来说 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征
  • Java是静态类型语言(尽管lambda表达式为其增加了动态特性)
  • Js、Python是动态类型语言
Java:String info = "Ali";   //静态语言

JS:var name = "Ali“;        //动态语言

Python: info = 150;         //更加彻底的动态语言

Ⅴ. 方法返回地址

  • 存放调用该方法的PC寄存器的值(使程序继续运行)
  • 一个方法的结束,有两种方式:

    • 正常执行完成
    • 出现未处理的异常,非正常退出
  • 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者(方法的调用者可能也是一个方法)的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出时,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息
  • 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去
  • 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值

Ⅵ. 栈帧中的一些附件信息

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息 例如,对程序调试提供支持的信息

相关文章
|
2月前
|
存储 算法 Java
惊!Java程序员必看:JVM调优揭秘,堆溢出、栈溢出如何巧妙化解?
【8月更文挑战第29天】在Java领域,JVM是代码运行的基础,但需适当调优以发挥最佳性能。本文探讨了JVM中常见的堆溢出和栈溢出问题及其解决方法。堆溢出发生在堆空间不足时,可通过增加堆空间、优化代码及释放对象解决;栈溢出则因递归调用过深或线程过多引起,调整栈大小、优化算法和使用线程池可有效应对。通过合理配置和调优JVM,可确保Java应用稳定高效运行。
117 4
|
11天前
|
存储 算法 Java
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
本文介绍了 JVM 的内存区域划分、类加载过程及垃圾回收机制。内存区域包括程序计数器、堆、栈和元数据区,每个区域存储不同类型的数据。类加载过程涉及加载、验证、准备、解析和初始化五个步骤。垃圾回收机制主要在堆内存进行,通过可达性分析识别垃圾对象,并采用标记-清除、复制和标记-整理等算法进行回收。此外,还介绍了 CMS 和 G1 等垃圾回收器的特点。
38 0
深入解析 Java 虚拟机:内存区域、类加载与垃圾回收机制
|
2月前
|
Java 索引
java中的栈(利用数组实现栈)
这篇文章通过Java代码示例介绍了如何使用数组实现栈操作,包括栈的初始化、入栈、出栈、判断栈满和空以及遍历栈的方法。
java中的栈(利用数组实现栈)
|
2月前
|
Java
Java常见JVM虚拟机指令(47个)
Java常见JVM虚拟机指令(47个)
49 3
Java常见JVM虚拟机指令(47个)
|
2月前
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
2月前
|
Java 数据安全/隐私保护 Windows
【Azure Developer】使用Java代码启动Azure VM(虚拟机)
【Azure Developer】使用Java代码启动Azure VM(虚拟机)
|
2月前
|
存储 Java API
【Azure Developer】通过Azure提供的Azue Java JDK 查询虚拟机的CPU使用率和内存使用率
【Azure Developer】通过Azure提供的Azue Java JDK 查询虚拟机的CPU使用率和内存使用率
|
3月前
|
监控 Oracle Java
(一)JVM成神路之初识虚拟机 - 探寻Java虚拟机的前世今生之秘
JVM(Java Virtual Machine)Java虚拟机的概念大家都不陌生,Java之所以可以做到“一次编译,到处运行”的跨平台性,其根本原因就在于JVM。JVM是建立在操作系统(OS)之上的,Java虚拟机屏蔽了开发人员与操作系统的直接接触,我们在通过Java编写程序时,只需要负责编写Java代码即可,关于具体的执行则会由JVM加载字节码后翻译成机械指令交给OS执行。
|
3月前
|
存储 Java 对象存储
Java虚拟机(JVM)中的栈(Stack)和堆(Heap)
在Java虚拟机(JVM)中,栈(Stack)和堆(Heap)是存储数据的两个关键区域。它们在内存管理中扮演着非常重要的角色,但各自的用途和特点有所不同。
42 0
|
2月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。