5.虚拟机栈

一、虚拟机栈概述

1.1 虚拟机栈出现的背景

  • 由于跨平台的设计,java的指令都是根据栈来设计的。不同平台cpu架构不同,所以不能设计为基于寄存器设计的。

1.2 虚拟机栈的优缺点

1.优点:跨平台,指令集小,编译器容易实现;
2.缺点:性能下降,实现同样的功能需要更多的指令

1.3 内存中的栈与堆

  • 栈是运行时的单位,而堆是存储的单位
    即:栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放在哪。
    堆和栈

1.4 java虚拟机栈基本内容

1.4.1 概念

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

1.4.2 生命周期

生命周期和线程一致

1.4.3 作用

主管java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
  • 一个栈帧对应着一个方法
    方法

1.4.4 栈的优点

  • 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
  • jvm直接对java栈的操作只有两个
    • 每个方法执行,伴随着进栈(入栈,压栈)
    • 执行后的出栈操作
  • 对于栈来说不存在垃圾回收问题
    栈

1.4.5 面试题

  • 面试题:开发中遇到的异常有哪些?
  • 栈中可能出现的异常:
    java虚拟机规范允许java栈的大小是动态的或者是固定不变的。
    1.如果采用固定大小的java虚拟机栈,那每个线程的java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个StackOverflowError异常
    2.如果java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请的足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个OutMemoryError异常。

1.4.6 设置栈内存大小

1.我们可以使用-Xss选项来设置线程的最大栈空间;
2.栈的大小直接决定了函数调用的最大可达深度;
  • 在idea 中run-configuration-VM options中设置,如:-Xss1024k
    1

二、 栈的存储单位

2.1 栈中存储什么

1.每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
2.在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)
3.栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息

2.2 栈的运行原理

  • jvm对java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出/后进先出”的原则
  • 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈被称为当前栈帧(Current ),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
  • 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
  • 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧
    2
    注意事项:
  • 不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧
  • 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
  • java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令,另一种是抛出异常。不管是使用哪种方式,都会导致栈帧被弹出。

2.3 栈帧的内部结构

每个栈中存储着:

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

3

三、 局部变量表

3.1 介绍

  • 局部变量表也被称之为局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类本数据类型、对象引用(reference),以及ReturnAddress类型
  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maxmum variables数据项中。在方法运行期间是不会改变局部变量表大小的。

4

3.2 关于Slot的理解

  • 局部变量表,最基本的存储单元是Slot(变量槽)
  • 参数值的存放总是在局部变量数组的index0开始,到数组长度-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处,其余的参数按照参数顺序继续排列。

3.3 补充说明

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

四、 操作数栈

4.1 介绍

  • 每一个独立的栈帧中除了包含局部变量表之外,还包含一个后进先出(Last-in-first-out)的操作数栈,也可以称之为表达式栈(Expression Stack)
  • 操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)
    • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。
    • 比如:执行复制、交换、求和等操作
      1
      图解:将8和15出栈,执行求和操作后再将结果进栈操作。

4.2 概念

  • 操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间
  • 操作数栈就是jvm执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
  • 每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的code属性中,为max_stack的值
  • 栈中的任何一个元素都是可以任意的java数据类型
    • 32bit的类型占用一个栈单位深度
    • 64bit的类型占用两个栈单位深度
  • 操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop)操作来完成一次数据访问

五、代码追踪

1.代码
1
2.过程解析
2
其中:push load都是入栈操作
store 是出栈操作

六、栈顶缓存技术

6.1 背景

由于操作数是存储在内存中的,因此频繁的执行内存读/写操作必然会影响执行速度。为了解决这个问题,HopSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理cpu的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

七、动态链接

`也叫指向运行时常量池的方法引用`

7.1 帧数据区

1

7.2 介绍

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

八、方法的调用:解析与分派

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

8.1 静态链接

当一个字节码文件被装载进jvm内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称为静态链接

8.2 动态链接

如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接

8.3 早期绑定和晚期绑定

1

8.4 非虚方法

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

tips:

子类对象的多态性的使用前提:
	1.类的继承关系
	2.方法的重写

8.5 方法调用指令

2

8.6 方法重写的本质

12

8.7 虚方法表

22

九、方法返回地址

9.1 介绍

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

十、栈的相关面试题

1.举例栈溢出的情况

答:StackOverflowError,通过-Xss设置大小,
1.局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。
2.递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。
3.指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。

2.调整栈大小,就能保证不出现栈溢出吗?
答:不能,如果说一个执行6000次递归就结束的程序,在可执行递归5000次的内存空间中会出现StackOverflowError,而在调整为可递归7000次的内存空间时就不会出现StackOverflowError。 如果程序是类似死递归情况无论怎样调整栈大小,都会出现溢出现象,对于这种情况,调整栈的大小理论上只能说是出现异常的时间晚一些而已

3.分配栈内存越大越好吗

不是,理论上好像会在一定时间或单位时间内减少StackOverflowError的概率,避免出现StackOverflowError过早,但是避免不了出现StackOverflowError。对栈空间来说是好事,但是这样会导致可执行的线程数减少,对其他内存结构也是有影响的

4.垃圾回收是否会设计到虚拟机栈

不涉及,虚拟机栈会出现StackOverflowError,但是不会出现GC,因为只有进栈出栈,出栈的过程就相当于GC(程序计数器不会Error和GC,堆会GC和ERROR,方法区也会出现GC和ERROR)

5.方法中定义的局部变量表是否线程安全?

何为线程安全?
 1.如果只有一个线程可以操作此数据,则必是线程安全的。
 2.如果有多个线程操作此数据,则此数据是共享数据,如果不考虑同步机制的话,会存在线程安全问题。
具体问题具体分析:
1.如果数据是在内部产生,内部消亡的,则是线程安全的
2.不是内部产生,或者作为返回值返回的(生命周期没有结束)就不是安全的
  • 举例:
    12
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值