运行时数据区 - 虚拟机栈
① 概述由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点:跨平台,指令集小,编译器容易实现。缺点:性能下降,实现同样的功能需要更多的指令。是什么?Java虚拟机栈,早期也叫Java栈。每个线程再创建时都会创建一个相对应的虚拟机栈,其内部保存着一个个栈帧,对应着一次次的Java方法调用。虚拟机栈是线程私有的。生命周期与线程的生命周期相同。
① 概述
由于跨平台性的设计,Java
的指令都是根据栈来设计的。不同平台CPU
架构不同,所以不能设计为基于寄存器的。
优点:跨平台,指令集小,编译器容易实现。
缺点:性能下降,实现同样的功能需要更多的指令。
- 是什么?
Java虚拟机栈
,早期也叫Java栈
。每个线程再创建时都会创建一个相对应的虚拟机栈,其内部保存着一个个栈帧,对应着一次次的Java
方法调用。虚拟机栈是线程私有的。
- 生命周期
与线程的生命周期相同。
- 作用
管理Java
程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
优点:
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
Jvm
直接对Java
栈的操作只有两个:
- 每个方法执行,伴随着入栈。
- 执行结束后的出栈工作。
对于栈来说不存在垃圾回收问题。
栈中常见异常:
由于Java
虚拟机允许Java
栈的大小是动态的或者是固定不变的。
如果采用固定大小的虚拟机栈,那每一个线程的虚拟机栈容量将再线程创建的时候独立选定。如果线程请求分配的栈容量超过了虚拟机栈允许的最大容量,则虚拟机会抛出一个StackOverflowError
异常。
如果采用动态扩展的虚拟机栈,如果线程尝试扩展虚拟机栈的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那虚拟机会抛出OutOfMemoryError
异常。
如何设置栈大小(IDEA中):
run -> Edit Configurations -> VM options:-Xss256k
单位有 k / m / g
② 栈的存储单位
栈中的数据都是以栈帧的格式存在的。
在栈中运行的每个方法都对应一个栈帧。
栈帧是一个内存块,是一个数据集,这个数据集维系着方法执行过程中的所需数据信息。
执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
栈帧的内部结构:
- 局部变量表。
- 操作数栈。
- 动态链接。
- 方法返回地址。
- 一些附加信息。
③ 局部变量表/本地变量表
- 局部变量表定义为一个数字数组,主要用于存储方法参数和定义在方法内部的局部变量。这些数据类型包括各种基本数据类型、对象引用、以及
returnAddress
类型。 - 由于栈为线程独享的,所以不存在线程安全问题。
- 局部变量表所需的容量大小在编译期就已经确定下来了,并保存在方法的
Code
属性的maximum local variables
数据项中。在方法的运行期间是不会改变局部变量表的大小的。
如何查看一个方法的局部变量表大小?
public class Main {
public static void main(String[] args) {
Main main = new Main();
int i = 100;
}
}
编译后使用javap -v Main.class
指令将字节码反汇编为汇编语言,其中标注了局部变量表大小。
或
查看局部变量表大小。
Slot
局部变量表最基本的存储单元:
上面说过,局部变量表是一个int[]
,其特性和int[]
相同,从0
位置开始到length - 1
。
最基本的存储单元为Slot
即变量槽。
其中:32位
以内的类型只占用一个变量槽,64位
的类型占用两个变量槽。
byte,short,char,boolean,float
在存储前被转换为int
且占据一个Slot
。long,double
在存储前被转换为int
且占据两个Slot
。
静态方法比非静态方法多存储一个this
变量。
Slot
重复利用:
栈帧中的局部变量表中的槽位是可以被重复利用的,如果一个局部变量过了其作用域,那么之后申请的新的局部变量就会复用之前的局部变量槽位。
//局部变量表大小为 3
public void test() {
int a = 0;
{
int b = 0;
b = a + 1;
}
//变量 c 使用的是局部变量 b 的槽位。
int c = a + 1;
}
④ 操作数栈
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
操作数栈就是JVM
执行引擎的一个工作区域,当一个方法开始执行的时候,一个新的栈帧被随之创建,那么这个操作数栈也是空的。
操作数栈的表现为一个一维整型数组即int[]
,那么当这个int[]
被创建的时候,即使是空的,那么也是有长度的。
栈中的任何一个元素都可以是任意的Java
数据类型。
32bit
数据占一个单位深度。62bit
数据占两个单位深度。
⑤ 操作数栈的代码演示
//Java代码
public void test() {
int a = 3;
int b = 1;
int c = a + b;
}
//字节码
0 iconst_3 //将3存入操作数栈中
1 istore_1 //将3从操作数栈中取出并放入局部变量表中索引为1的位置(因为0位置存放的是this)
2 iconst_1 //将1存入操作数栈
3 istore_2 //将1从操作数栈取出,存入局部变量表索引为2的位置
4 iload_1 //取出局部变量表索引为1的值,存到操作数栈中
5 iload_2 //取出局部变量表索引为2的值,存到操作数栈中
6 iadd //相加并出栈 得到结果继续存入操作数栈 此时操作数栈只有一个值,即相加的结果
7 istore_3 //将相加结果存入到局部变量表索引为3的位置
8 return
⑥ 栈顶缓存技术
由于操作数是存储于内存中的,因此频繁地执行内存读/写操作必然会影响速度。
所以HotSpot JVM
设计师们提出了栈顶缓存技术:
将栈顶元素全部缓存在物理CPU
的寄存器中,一次降低读取次数,提升执行效率。
⑦ 动态链接
https://www.bilibili.com/video/BV1PJ411n7xZ?p=55
⑧ 方法的调用:解析与分配
-
早期绑定:早期绑定指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法于所属的类型进行绑定。因此也就可以使用静态链接的方式将符号引用转换为直接引用。
-
晚期绑定:如果被调用的方法在编译期无法确定,只能在程序运行期间根据实际的类型绑定相关的方法,这种绑定方式称为晚期绑定。
**非虚方法:**不涉及到多态
- 如果方法在编译期就确定了具体调用版本,并且这个版本在运行时不可变。这样的方法称为非虚方法。例如:抽象发放不是非虚方法,因为它会被所有它的实现类重写。
- 静态方法、私有方法、
final
方法、实例构造器、父类方法都是非虚方法。
字节码中的体现:
invokestatic
:调用了静态方法、解析阶段确定唯一方法版本。invokespecial
:调用了<init>
方法、私有或父类方法。解析阶段确定唯一版本。invokevirtual
:调用了虚方法。调用final
也会出现此条字节码,但final
方法不是虚方法。invokeinterface
:调用了接口方法。invokedynamic
:动态解析出需要调用的方法。
⑨ 方法返回地址
存放调用该方法的pc
寄存器的值。
也就是将本方法的返回值压入调用方的操作数栈。
⑩ 一些附加信息
略。
⑩① 相关面试题
举例栈溢出的情况?
StackOverflowError
:压栈超过了栈最大容量,或递归没有终止条件。
可以通过-Xss
设置栈大小来解决,当然后者无法解决。
调整栈大小,就能保证不溢出了吗?
不能保证的,递归没有中止条件的时候不能解决栈溢出。
分配的栈内存越大越好吗?
肯定不是越大越好。对自己线程好,但是对别人就不好了。
垃圾回收涉及到栈空间吗?
不涉及。
方法中定义的局部变量是否线程安全?
具体问题具体分析。
如果方法中开启线程,公用方法中局部变量,则不安全。
如果没有开启线程,或开启外部线程,则是线程安全的。
更多推荐
所有评论(0)