05-虚拟机栈及相关问题
虚拟机栈 (线程私有)
不存在GC,存在OOM
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次次的方法调用
虚拟机栈的生命周期和线程一致。作用是主管java程序的运行,保存方法的局部变量(8种基本数据类型,对象的引用地址,对象实际存储在堆空间中),部分结果,并参与方法的调用和返回
栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器
JVM直接对栈的操作:
1、每个方法执行,伴随着进栈(入栈,压栈)
2、执行结束后的出栈操作
设置栈内存的大小
使用参数-Xss选项设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度
栈的存储单位
1)栈中的数据都是以栈帧为基本单位存在
2)在这个线程上正在执行的每个方法都各自对应着一个栈帧
3)栈帧是一个内存区块,是一个数据集,****维系着方法执行过程中的各种数据信息
4)在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,与当前栈帧相对应的方法就是当前方法,定义这个方法的类就是当前类。
5)执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
6)如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
栈帧的内部结构
每个栈帧都存储着:
- 局部变量表
- 操作数栈(表达式栈)
- 动态链接(或指向运行时常量池的方法引用)
- 方法返回地址(或方法正常退出或异常退出的定义)
- 一些附加信息
方法嵌套调用的次数由栈的大小决定,栈越大,方法嵌套调用的次数就越多。
对于一个函数,参数和局部变量越多,局部变量表就会膨胀,栈帧就越大
局部变量表
1、存放编译器的各种基本数据类型(8种)引用类型(reference)returnAddress类型的变量
2、定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量
3、由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
4、局部变量表所需的容量大小是在编译器就确定下来的(一旦确定就不会更改)
注意:局部变量表中的变量只在当前方法调用中有效,当方法调用结束后,随着方法栈帧的销毁,局部变量表也会销毁
1 | /** |
比如此处,num对应的起始pc是8,对应代码的实际行数第14行,字节码指令长度是17
所以起始pc加上长度就是字节码的指令长度
textcolor{red}{局部变量表的基本存储单元是Slot(变量槽)
- 32位以内的类型只占用一个Slot(包括returnAddress类型)
- 64位的类型占用两个Slot(long和double)
- byte,short,char在存储前转化为int
- boolean也被转换为int,0表示false,1表示true
- JVM会为局部变量中的每一个Slot都分配一个访问索引,通过这个索引可以访问局部变量中指定的值
如果需要访问一个64bit的局部变量值时,只需要使用前一个索引即可
如果当前帧是由构造方法或者实例的方法进行创建的,那么该对象引用this将会存放在index为0的Slot处,其余的继续排列
例子:
*this代表对象实例,而static是随类的加载而加载,先于实例之前就有的,所以不存在于局部变量表中,所以不能使用。
变量的分类 一、按照数据类型分:1、基本数据类型 2、引用数据类型
二、按照在类中声明的位置分: 1、成员变量:在使用前都经过默认初始化赋值
类变量:linking的prepare阶段,给类变量默认赋值 —> initial阶段,给类变量显式赋值即静态代码块赋值
**实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值**
**2、局部变量:使用前必须进行显示赋值,否则编译不通过**
局部变量表中的变量是重要的垃圾回收根节点,只要被局部变量表中的直接或间接引用的对象都不会被回收
操作数(表达式)栈
- 每一个独立的栈帧中除了包含局部变量表以外,还包含了一个后进先出的操作数栈,也可以称之为表达式栈
- 在方法执行的过程中,根据字节码指令,往栈中写入数据(入栈)或提取数据(出栈)
- 主要用于保存计算过程的中间结果,同时作为计算机过程中变量临时的存储空间
- 当一个方法开始执行的时候,一个新的栈帧也会被创建出来,这个方法的操作数栈是空的
- 每一个操作数栈都会拥有一个栈深度用于存储数值(max_stack)32bit类型占用一个栈单位深度,64bit类型占用两个栈单位深度
- 操作数栈不是通过访问索引的方式来进行数据访问的,而只能通过标准的入栈出栈来进行数据访问
代码追踪
1 | public void testAddOperation(){ |
操作数栈的最大深度
1 | public class OperandStackTest { |
栈顶缓存技术
为了解决指令过多导致频繁地执行内存读/写操作,影响执行速度的问题
将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数
(将计算的操作放到CPU寄存器里面去)
动态链接
栈帧内部包含了一个指向运行时常量池中该栈帧所属方法的引用—动态链接
每一个栈帧内部都包含一个指向运行时常量池Constant pool或该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令
动态链接的作用就是为了将这些符号引用(#)最终转换为调用方法的直接引用
方法的调用:
- 静态链接:在字节码文件装载进JVM内部时,被调用的方法在编译期可知,而且在运行的期间保持不变,这时候符号引用就会转换为直接引用。这个过程即为静态链接
- 动态链接:被调用的方法在编译期不可确定,在程序运行的期间才将符号引用进行转换,这个过程为动态链接
- 绑定:一个字段,方法,类在符号引用转换为直接引用的过程
- 早期绑定:被调用的目标方法在编译期内可知,运行期间不变
- 晚期绑定:调用方法在编译期无法确定,到运行期才确定
- 非虚方法:在编译器确定调用的版本,这个版本运行时不变,比如:静态方法,私有方法,final方法,实例构造器,父类方法
- 虚方法:其他
虚方法表
每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
如果类中重写了方法,那么调用的时候,就会直接在虚方法表中查找,否则将会直接连接到Object的方法中
虚方法表会在类加载的链接阶段被创建并开始初始化 ,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。
方法返回地址
存储调用该方法的PC寄存器的值
在方法推出后都会返回到该方法被调用的位置。
方法正常退出时,调用者的PC计数器的值作为返回地址(调用该方法的指令的下一条指令的地址)
方法异常退出时,返回地址通过异常表来确定
相关问题
1、举例栈溢出的情况:通过-Xss设置栈的大小(OOM)
2、调整栈大小,就能保证不出现溢出吗?不能
3、分配的栈内存越大越好吗?不是,栈空间大了线程数就少了,栈空间和线程数目成反比
4、垃圾回收是否会涉及到虚拟机栈?不会
5、方法中定义的局部变量是否线程安全?看情况
6、代码演示:(内部产生,在内部消亡,线程安全)
1 | //s1的声明方式是线程安全的 |