JVM_虚拟机栈详解


虚拟机栈

1. 虚拟机栈出现的背景
  • 由于跨平台性的设计,Java 的指令都是根据栈来设计的。不同平台的 CPU 架构不同,所以不能设计为基于寄存器的。

  • 优点:跨平台、指令集小、编译器容易实现

  • 缺点:性能下降、实现同样的功能需要更多的指令

1.1 内存中的栈与堆

栈是运行时的单位,而堆是存储的单位。
即:

  • 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据
  • 堆解决的好似数据存储的问题,即数据怎么放,放在哪儿
2 虚拟机栈基本内容
2.1 Java 虚拟机栈是什么?

Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。
是线程私有的

2.2 生命周期

生命周期和线程一致

2.3 作用

主管 Java 程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。
> 局部变量 VS 成员变量(或属性)
> 基本数据变量 VS 引用类型变量(类、数组、接口)

2.4 栈的优点
  1. 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
  2. JVM 直接对 JAVA 栈的操作只有两个:
    • 每个方法执行,伴随着进栈(入栈、压栈)
    • 执行结束后的出栈工作
  3. 对于栈来说 不存在垃圾回收 问题
2.5 栈中可能出现的异常

Java 虚拟机规范允许 Java 栈的大小是动态的或者是固定不变的

  1. 如果采用固定大小的 Java 虚拟机栈,那每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个 StackOverflowError 异常。
  2. 如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。
2.6 设置栈内存大小

使用参数 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度

2.7 栈中存储什么?
  • 每个线程都有自己的栈,栈中的数据都是以 栈帧(Stack Frame) 的格式存在
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
2.8 栈运行原理
  • JVM 直接对 Java 栈的操作只有两个,就是对栈帧的 压栈出栈,遵循“先进后出” / “后进先出” 原则
  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为 当前栈帧(Current Frame) ,与当前栈帧相对应的方法就是当前方法 (Current Method) ,定义这个方法的类就是 当前类(Current Method)
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前帧
  • 不同的线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧
  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新称为当前栈帧
  • Java 方法有两种返回函数的方式,一种是正常的函数返回,使用 return 指令;另一种是抛出异常,不管使用哪种方式,都会导致栈帧被弹出
3. 栈帧的内部结构

每个栈帧中存储着:

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)(或表达式栈)
  • 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
  • 方法返回地址(Return Address) (或方法正常退出或者异常退出的定义)
  • 一些附加信息

栈帧的内部结构

3.1 局部变量表
  • 局部变量表也被称之为局部变量数组或本地变量表

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

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

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

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

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

  • 关于 Slot 说明

    • 参数值的存放总是在局部变量数组的 index0 开始,到数组长度-1的索引结束
    • 局部变量表 最基本的存储单元是 Slot (变量槽)
    • 局部变量表中存放编译其克制的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量
    • 在局部变量表里,32位以内的类型只占用一个 slot (包括 returnAddress 类型),64位的类型( long 和 double )占用两个 slot

      byte 、short 、char 在存储前被转换为 int ,boolean 也被转换为 int ,0 表示 false 、非0表示 true
      long 和 double 则占据两个 Slot

  • 补充说明

    • 在栈帧中,与性能调优关系最为密切的部分就是局部变量表,在方法执行时,虚拟机使用局部变量表完成方法的传递
    • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
3.2 操作数栈
  • 栈可以使用数组或链表来实现

  • 每个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称为 表达式栈

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

    • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用它们后再把结果压入栈
    • 比如执行复制、交换、求和等操作
  • 操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间

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

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

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

    • 32 bit 的类型占用一个栈单位深度
    • 64 bit 的类型占用两个栈单位深度
  • 操作数栈 并非采用访问索引的方式来进行数据访问 的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问

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

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

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

3.3 动态链接
  • 每一个栈帧内部都包含一个指向 运行时常量池该栈帧所属方法的引用 。包含这个引用的目的就是为了支持当前方法的代码能够实现 动态链接(Dynamic Linking) 。比如:invokedynamic 指令
  • 在 Java 源文件被编译到字节码文件中时,所有的变量和方法引用都作为 符号引用(Symbolic Reference) 保存在 class 文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量此中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用

在这里插入图片描述
为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。

3.4 栈帧内部结构

在这里插入图片描述

3.5 方法的调用

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

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

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

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

随着高级语言的横空出世,类似于 Java 一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然 这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定这两种绑定方式

Java 中任何一个普通的方法其实都具备虚函数的特征,它们相当于 C++ 语言中的虚函数(C++ 中则需要使用关键字 virtual 来显式定义)。如果在 Java 程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字 final 来标记这个方法

3.5.1 虚方法与非虚方法

非虚方法

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

虚拟机中提供一下几条方法调用指令:

  1. 普通调用指令:
    • invokestatic 调用静态方法,解析阶段确定唯一方法版本
    • invokespecial 调用< init > 方法、私有及父类方法,解析解读那确定唯一方法版本
    • invokevirtual 调用所有虚方法
    • invokeinterface 调用接口方法
      2.动态调用指令:
    • invokedynamic 动态解析出需要调用的方法,然后执行

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而 invokedynamic 指令则支持由用户确定方法版本。其中 invokestatic 指令和 invokespecial 指令调用的方法称之为非虚方法,其余的(final 修饰的除外)称为虚方法

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

Mr.superbeyone


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

superbeyone

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值