JVM系列(四):虚拟机栈

1、概述及其定义

1.1、概述

  • Java的指令根据来设计,因为不同平台CPU架构不一样,所以不能设计为基于寄存器。
  • 基于栈的优点:跨平台、指令集小、编译器容易实现;
  • 基于栈的缺点:性能下降、实现同样功能需要更多指令(指令集小、功能单一)
  • :运行时的单位,程序如何执行、如何处理数据(方法);
  • :存储时的单位,解决数据的存储问题,数据放在哪里(对象)

1.2、Java虚拟机栈

  • 早期称Java栈;
  • 每个线程创建时都创建一个栈,内部保存的单位是栈帧,对应一次次方法调用;
  • 生命周期与线程一致;
  • 主管Java程序运行,保存方法的局部变量、部分结果,参与方法的调用和返回;
  • 特点(优点):速度快(仅次于程序计数器),只有两种操作(出栈入栈),不存在垃圾回收问题。
  • 异常:JVM允许栈大小固定或动态,固定大小,线程请求分配容量超过,则StackOverflowErrow异常;动态拓展,没有足够的内存时抛出OutMemoryError错误。
  • 设置栈内存大小,通过-Xss参数设置线程的最大空间,栈的大小决定函数调用的最大深度

2、栈的存储单位

2.1、栈存储

  • 每个线程有自己的栈,数据以栈帧格式存在;
  • 线程上每个方法对应一个栈帧;
  • 栈帧是一个内存区块,一个数据集,存着方法执行过程中各种数据信息;

2.2、栈运行原理

  • 两个操作,出栈入栈,先进后出/后进先出;
  • 一个线程一个时间点上只有一个活动帧(当前帧),对应当前方法,定义该方法的类就是当前类;
  • 执行引擎的所有字节码指令仅针对当前帧;
  • 方法中调用其他方法,新建栈帧,放在栈顶成为当前帧;
  • 不同线程中的栈帧不允许相互引用,即不可能在一个栈帧中引用另一个线程的栈帧
  • 方法调用中,当前帧将传回执行结果给前一个帧,接着虚拟机丢弃当前栈帧; Java两种返回形式:正常返回return;抛出异常,沿着栈帧一直回传到有处理的栈帧(方法),如果一直没有处理,则线程结束,报错。

2.3、栈帧的内部结构

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

3、局部变量表

  • 定义为一个数字数组,用于存储方法参数、定义在方法体内的局部变量,数据类型包括:基本数据类型、对象引用、returnAddress等;
  • 局部变量表建立在线程的栈上,线程私有,所以不存在数据安全问题;
  • 局部变量表大小在编译时就确定,保存在maxinum local variables数据项中,运行时局部变量表大小不变;
  • 方法嵌套调用的次数由栈的大小决定,栈的大小确定了,栈帧大,则能存的栈帧就少了;
  • 局部变量表中的变量只在当前方法调用中有效,方法调用结束后,局部变量表随之销毁;

3.1、局部变量表-slot(变量槽)

  • 局部变量表中最基本的存储单元;
  • 存放8种基本数据类型、引用类型、returnAddress类型变量;
  • 32位数据(引用类型也是占一个slot)占一个slot,64位(double、long)占两个slot;
  • JVM给每个slot分配一个访问索引,通过索引可以成功访问局部变量表中的局部变量值;
  • 64位的局部变量占2个slot,有两个索引,按照第一个索引访问;
  • 如果当前帧是由构造方法或者实例方法创建的,那么将对象引用(this关键字)存放在index=0的slot处,其余的按顺序存储;
  • Slot重复利用:如果一个局部变量过了其作用域,那么其开辟的slot会给新的局部变量使用,从而节省资源。

3.2、变量

  • 按照数据类型分:基本数据类型、引用数据类型
  • 按照在类中的位置分:

     ① 成员变量:使用前经历过默认初始化

           类变量(静态变量):链接的准备阶段,给类变量默认赋值(初始化阶段赋值)

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

     ② 局部变量:使用前需要显示赋值,存在栈 -> 栈帧 -> 局部变量表 -> slot

3.3、补充

  • 局部变量表和性能调优关系最为密切,方法执行时使用局部变量表完成方法参数传递;
  • 局部变量表中的变量是重要的垃圾回收跟节点,只要被局部变量表中变量直接引用或间接引用的对象都不会被回收

4、操作数栈(表达式栈)

4.1、操作数栈定义及功能

  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据(入栈、出栈);
  • 主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间;
  • JVM执行引擎的一个工作区,方法执行时,新的栈帧创建,此时操作数栈初始化,为空;
  • 每个操作数栈都有明确的栈深度用于存储数值,最大深度在编译时定义,保存在Code属性中,max_stack值;
  • 栈中元素可以是任意Java类型,32位占一个栈单位深度,64位两个;
  • 操作数栈仅能通过入栈出栈操作,不能通过索引访问;
  • 被调用的方法含有返回值,其返回值被压入调用者当前栈帧的操作数栈中,并更新PC寄存器中下一条要执行的字节码指令
  • 操作数栈中元素数据类型需要与字节码指令中一致,编译时会验证,类加载中的类检验阶段的数据流分析阶段再次验证;
  • Java虚拟机的解释引擎基于栈,其中的栈指的就是操作数栈;

4.2、栈顶缓存技术

  • 操作数存储在内存中,频繁的读取/写操作影响执行速度,为了解决这个问题,提出栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升引擎执行效率。

5、动态链接

  • 动态链接、方法返回地址、一些附加信息也合称为帧数据区
  • 动态链接也称指向运行时常量池的方法引用
  • 每个栈帧内部都包含一个指向运行时常量池该栈帧所属方法的引用,实现动态链接;
  • Java代码编译成字节码文件,所有的变量和方法引用都作为符号引用保存在class的常量池中,动态链接就是为了将这些符号引用转为调用方法的直接引用;(将描述转为实际地址)
  • 常量池的作用就是为了提供一些符号和常量,便于指令的识别;

6、方法返回地址

  • 存放调用该方法的PC寄存器的值

  • 方法结束有两种:正常结束;异常结束。方法退出后,要返回到该方法被调用的位置(但是异常退出不会有返回值)。

  1. 正常退出,调用者的PC寄存器的值作为返回地址,即调用该方法的指令的下一条指令的地址

  2. 异常退出,返回地址通过异常表来确定,栈帧一般不存储这个信息。

  • 正常退出和异常退出的区别:异常退出,不会给上层调用者产生任何返回值。

  • 返回指令:ireturn(boolean byte char short int),lreturn、freturn、dreturn、areturn(引用类型)。Return(无返回)是void方法、实例初始化方法(构造器)、类和接口的初始化方法。

  • 方法执行过程中抛出异常时的异常处理,存储在一个异常处理表中,方便发生异常时找到相关处理代码。如果异常没有在方法内处理,也就是在本方法的异常表中没有搜索到匹配的异常处理器,方法退出。

  • 方法退出后,都必须返回到最初方法被调用时的位置,程序才能继续执行


7、一些附加信息

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


8、方法调用

8.1、动态静态链接

  • 静态链接:被调用的方法在编译期就可知,且运行期间保持不变,在这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接。

  • 动态链接:被调用的方法在编译期无法确定,只有在程序运行期间将符号引用转为直接引用,这个过程称为动态引用。

8.2、绑定

  • 绑定:一个字段、方法或类的符号引用被替换为直接引用的过程,仅发生一次

  • 早期绑定:针对静态链接,目标方法编译期可知,运行时不变。

  • 晚期绑定:针对动态连接,目标方法编译时无法确定,运行时根据实际绑定相关方法。

8.3、虚方法

  • Java中方法具有虚函数特征,如果不希望拥有,使用final关键字。

  • 非虚方法:(不涉及多态),方法在编译期间确定了调用版本,版本在运行时不变,包含静态方法、私有方法、final方法、构造器、父类方法

  • 虚方法:除了非虚方法之外都是(公有方法、多态中子类方法),就是运行时才确定具体调用的方法。

  •  虚方法表:

  1. 面向对象中,频繁使用动态分派,如果每次都在类方法元数据中搜索合适的目标,会影响效率。为了提升效率,JVM在类的方法区建立一个虚方法表,使用索引表来代替查找

  2. 每个类都有一个虚方法表,表中存着各个方法的实际入口

  3. 虚方法表在类加载的链接阶段创建并初始化,类的变量初始值准备完成后,JVM将该类的方法表也初始化完毕

  • 题外话:java中父类的私有方法可以被子类继承,但无法访问

class PersonTest{
    public void fun1() {
        fun2();
        System.out.println("fun1");
    }
    private void fun2() {
        System.out.println("fun2");
    }
}
class Man extends PersonTest {

}
public class Test7 {
    public static void main(String[] args) {
        Man man = new Man();
        man.fun1();
    }
}
  • 子类Man可以通过调用公有方法fun1,然后在fun1中调用私有方法fun2,但是不允许子类Man直接调用私有方法fun2

8.4、方法调用指令

  • 普通调用指令:
  1. Invokestatic:调用静态方法
  2. Invokespecial:调用构造方法、私有、父类方法
  3. Invokevirtual:调用所有虚方法
  4. Invokeinterface:调用接口方法
  • 动态调用指令:
  1. Invokedynamic:动态解析出需要调用的方法(为了实现动态类型语言支持改进)

8.5、方法重写的本质

  • 找到操作数栈顶的第一个元素执行的对象的实际类型,记作C;
  • 在类型C中找与常量中描述符合、简单名称符合的方法,则进行权限校验,如果通过,直接返回,过程结束;不通过,则返回java.lang.illegalAccessError异常
  • 找不到,则按照继承关系从下往上对C的父类进行上一步操作;
  • 一直找不到,抛出java.lang.AbstractMethodError异常;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值