运行时数据区——虚拟机栈

运行时数据区——虚拟机栈

  • 栈是运行时的单位,堆是存储单位。
  • 栈是线程私有的,每个线程创建的时候,都会创建一个虚拟机栈,栈里面是一个个栈帧,每个栈帧对应着一个方法的调用。栈的生命周期和线程保持一致。
  • 栈主管java程序的运行,保存方法的局部变量,部分结果,并参与方法的调用和返回。

栈的数据结构

1、栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
2、jvm直接对虚拟机栈的操作只有两个:(1)每个方法的执行,伴随着进栈;(2)执行结束后的出栈操作。
3、对于栈来说不存在垃圾回收问题。
在这里插入图片描述
java允许java虚拟机栈的内存固定不变或者动态扩容
1、固定不变:如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将抛出StackOverflowError错误。
在这里插入图片描述
在这里插入图片描述
2、动态扩容:在尝试扩展内存时,无法申请到足够的内存,或者在创建新的线程时,没有足够的内存创建虚拟机栈,java会抛出OutOfMemoryError错误,可以不断的创建线程,测试这个问题。官方文档:添加链接描述
在这里插入图片描述
使用参数【-Xss】选项设置线程的最大栈空间,栈的大小直接决定了方法调用的最大可达深度。

栈帧

  • 每个线程都有自己的栈,栈的数据都是以栈帧(stack frame)的格式存储的。
  • 在这个线程上正在执行的方法都对应各自的一个栈帧。
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各个数据信息。
  • 在一条活动线程中,一个时间节点上,只会有一个活动的栈帧,即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为“当前栈帧”,与当前栈帧对应的方法就是当前方法,定义这个方法的类就是当前类。
  • 在执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
    栈帧的内部结构
    1、局部变量表(local variables)
    2、操作数栈(Operand stack)
    3、动态连接(Dynamic linking)或 指向运行时常量池的方法引用。
    4、方法返回地址(return address)
    5、一些附加信息
    在这里插入图片描述

局部变量表

1、 定义一个数字数组,主要用于存储方法参数和定义在方法体的局部变量,这些数据类型包括各种基本数据类型,对象引用,以及return address 类型。
2、由于局部变量表是建立在线程栈上的,是线程私有数据,因此不存在线程安全问题。
3、局部变量表所需的容量大小是在编译期确认下来的,并保存在方法的code属性的maximun local variables数据项中,在方法运行期间是不会改变局部变量表大小的。
4、方法嵌套调用次数,由栈的大小决定,一般来说栈越大,方法嵌套调用次数就越多,对于一个函数而言,它的参数和局部变量越多,使得局部变量表越大,那么栈帧就会越大,栈帧打了,栈能存储的栈帧就少了。
5、局部变量表中的变量只在当前调用方法中有效,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当前方法调用结束后,随着当前方法栈帧的销毁,局部变量表也随之销毁。
在这里插入图片描述
过于slot的理解:
1、 参数的存储总是在局部变量数组的index0开始,到数组长度-1,到索引结束。
2、局部变量表,最基本的存储单元是slot(变量槽)
3、局部变量表中存放编译期可知的各种基本数据类型、引用类型、return address类型的变量。
4、在局部变量表里,32位以内的类型只占用1个slot(包括return address 类型),64位的类型(long/double)占用两个slot。
5、byte、short、char在存储前都会被转换成int,Boolean和float也被转换成int、0=false、1=true。Long和Double占用两个slot。
6、JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可访问到局部变量表中指定的局部变量值。
7、当一个方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每个slot上。
8、如果需要访问局部变量表中,一个64bit的局部变量值时,只需要使用当前一个索引即可(比如访问long或者double类型变量)
9、如果当前栈帧是由构造方法或者实例方法创建的,那么该对象引用.this将会存放在index为0的slot上,其余参数按照参数列表顺序继续排列。
在这里插入图片描述
在这里插入图片描述
栈帧中的局部变量表是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后,申明的新的局部变量就很有可能会复用过期局部变量表的槽位,而达到节省资源的目的。
在这里插入图片描述
变量i的作用域是8开始向下数5个指令。
在这里插入图片描述
那么变量i作用域过后,定义了变量c,这时候变量c就可以用之前变量i的slot。
在这里插入图片描述
在这里插入图片描述
一个有5个slot,this占用1slot、a占用1slot、b占用1slot,i占用2slot、c复用i的1slot,一共5个slot
在这里插入图片描述
其实就是slot被复用了,因为i已经过了作用域,c就复用了i的slot,这样是为了节省空间,本来需要6个slot,但实际只只用4个,因为i需要两个,所以在最大槽数是5.

静态变量与局部变量的对比
变量分类:
1、按照数据类型:基本数据类型、引用数据类型
2、按照类中的申明位置:
a、成员变量:在使用前都经历默认的初始化赋值
ⅰ、类变量:在linking的prepare阶段,给变量默认赋值,initial阶段,给类变量显示赋值,即静态代码块赋值
ⅱ、实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值。
b、局部变量:使用前,必须要进行显示赋值,否则编译不通过。
在栈帧中,与性能调优关系最为密切的部分,就是局部变量表,在方法执行时,虚拟机使用局部变量表,完成方法的传递。
局部变量表的变量也是重要的垃圾回收根节点,只要被局部变量表中的直接引用或者间接引用的对象都不会被回收。

操作数栈:

● 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据、或者提取数据,即入栈、出栈。
● 某些字节码,将值压入操作数栈,其余的字节码指令将操作数去除栈,使用它们后,再把结果压入栈,如:执行复制,交换,求和等。
● 操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
● 操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。
● 每个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就决定好了,保存在方法的code属性中,为max——stack的值。
在这里插入图片描述
● 栈中的任何一个元素都可以是java的任意数据类型。(32bit的类型占用一个栈单位深度、64bit的类型占用两个栈单位深度)
● 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问。
● 如果被调用的方法带有返回值,其返回值会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
● 操作数栈中的元素的数据类型必须与字节码指令的顺序严格匹配,这由编译器在编译期间验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。
● 另外,我们说java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。
在这里插入图片描述

栈顶缓存技术【了解】

动态链接(或指向运行时常量池的方法引用)

每个栈帧内部都包含一个指向运行时常量池中该栈帧所属的方法引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。
在这里插入图片描述
比如:描述一个方法调用另外的其他方法时,就是通过常量池中指向方法的符号引用来表示。
那么动态链接的作用就是:为了将这些符号引用转换成调用方法的直接引用。
在这里插入图片描述
方法的调用原理:
在JVM中,将符号调用转换为调用方法的直接引用与方法的绑定机制有关。
静态链接:
当一个字节码文件被装载近JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接。
动态链接:
如果被调用的方法在编译期无法被确定下来,也就是说只能在程序运行期将调用方法的符号引用转换成直接引用,由于这种引用转换过程具备动态性,因此被称之为动态链接。

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

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

虚拟机中提供了几个方法的调用指令:
普通调用指令:
invokestatic:调用静态方法,解析阶段确定唯一方法版本
invokespecial:调用方法,私有及父类方法,解析阶段确定唯一方法版本
invokevirtual:调用所有虚方法
invokeinterface:调用接口方法
动态调用指令:
invokedynamic:动态解析需要调用的方法,然后执行。

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

虚方法表:
在面向对象的编程中,会很频繁的使用到动态分派,如果每次动态分派的过程中都需要重新在类的方法数据中搜索适合的目标的话就可能会影响性能。因此:为了提高效率JVM采用在类的方法区建立一个虚方法表(非虚方法不会出现虚方法表中)来实现,使用索引来代替查找。
每个类中都有一个虚方法表,表中存放各个方法的实际入口。虚方法表会在类加载的链接阶段被创建出来并初始化,类的变量初始值准备完成后,JVM会把该类的方法表也初始化完毕。

方法返回地址

存放调用该方法的PC寄存器的值。
一个方法的结束有两种方式

  1. 正常执行完成返回
  2. 出现未处理的异常,非正常退出

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

字节码指令中返回指令包含

  1. ireturn:当返回值是Boolean、byte、char、short、int类型的使用
  2. lreturn:当返回值是long类型时
  3. freturn:当返回值是float类型时
  4. dreturn:当返回值是double类型时
  5. areturn:返回值是引用类型时使用
  6. return:void方法,实例初始化方法,类和接口的初始化方法

一些附加信息

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

方法中定义的局部变量是否线程安全?
变量是方法内部产生,且都在内部消亡就是线程安全的,否则不是。

本地方法接口:
一个native method就是一个java调用非java代码的接口。
标识符native可以与其它所有的java标识符连用,但是abstract除外,因为abstract没有方法体,但native方法有方法体,只是native方法体不是java语言写的。

本地方法栈:

本地方法栈管理本地方法的调用,线程私有的允许被实现成固定或动态扩展的内存大小。
固定——StackOverflowError 动态扩容——OutOfMemoryError
本地方法是使用C语言写的,它的具体做法是在native method stack中登记native方法在execute engine执行时加载本地方法库

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值