目录
- Java虚拟机栈相关知识总结
- Java虚拟机栈是什么?
- 虚拟机栈大小的调整
- 局部变量表
- 操作数栈
- 动态链接
- 方法返回地址
- 附加信息
- 相关问题
- 1.什么情况下会发生栈内存溢出?
- 2.如果让你写一段栈溢出的代码你会什么写
- 3.一个栈大概有多大?
- 4.每个线程都有这样大小的一个栈吗?
- 5.JVM 栈中存储的是什么
- 6.Java 对象会不会分配到栈中?
Java虚拟机栈相关知识总结
Java虚拟机栈是什么?
每个方法被调用的时候,都会相应的在Java虚拟机栈中创建栈帧,虚拟机栈描述的是Java方法执行的内存模型:
- Java虚拟机栈是线程私有的,其生命周期与线程一致
- 栈帧包括 局部变量表、操作数栈、动态链接、方法返回地址 和 一些附加信息
- 每个方法的调用直至结束,都对应着一个栈帧在java虚拟机栈中从入栈到出栈的过程
虚拟机栈大小的调整
Java虚拟机规范允许虚拟机大小固定不变或者可动态扩展
- 固定不变:如果线程请求分配的栈容量超过了虚拟机允许的最大容量,则抛出StackOverflowError异常
- 可动态扩容:尝试扩容时无法申请到足够的内存,或者在创建线程时没有足够的内存去创建对应的虚拟机栈,则会抛出OutOfMemoryError异常
局部变量表
- 用来存储方法中的所有局部变量值,包括传递的参数
- JVM在编译器就决定好了局部变量表的大小
- 一个slot(槽位)可以保存一个类型为
boolean
、byte
、char
、short
、int
、float
、reference
或returnAddress
的数据 -
long
、double
类型占用两个连续的slot,采用较小的索引值定位 - 成员方法第一个slot存放的是this,指向当前对象
- 如果当前程序计数器的值超出了某个变量的作用域,那么这个变量对应的slot就可以被其他变量使用(slot被复用)
操作数栈
用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。其最大深度在编译时就被写到了Code属性的max_stacks中
- 栈中的元素可以是java的任意类型,32bit的类型占用一个栈深度,64bit的类型占用两个栈深度
动态链接
- 每个栈帧都包含一个指向当前方法所在类型的运行时常量池的引用
- Class常量池中存在大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数,动态链接的作用就是将这些以符号引用表示的方法转换为对实际方法的直接引用
- 部分符号引用会在类加载阶段或第一次使用时转换为直接引用,这种转换成为静态解析
- 另一部分将在每一次运行期间转换为直接引用,这部分成为动态链接
方法返回地址
正常完成:
执行引擎遇到方法返回的字节码指令,这种时候可能会有返回值传递给上层调用者
异常完成:
方法执行过程中遇到异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,一个方法使用异常完成出口的方式退出,一定不会有方法返回值给它的调用者
无论采用哪种退出方式,方法退出后都需要返回到方法被调用的位置,当前栈帧承担着恢复调用者状态的责任。方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中可能会保留这个计数器。方法异常退出时,返回地址要通过异常处理器表来确定
方法退出的过程就是将当前栈帧出栈,恢复上次方法的局部变量表和操作数栈,如果有返回值,则将返回值压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令的后一条指令等。
附加信息
虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。
相关问题
1.什么情况下会发生栈内存溢出?
如果线程请求的栈深度大于虚拟机所允许的深度,将抛出StackOverflowError异常
2.如果让你写一段栈溢出的代码你会什么写
public static void main(String[] args) {
main(args);
}
3.一个栈大概有多大?
JDK8中:
- Window下,默认值取决于虚拟机内存
通过jinfo -flag ThreadStackSize
查看可以得到-XX:ThreadStackSize=0
- Linux,MacOS下:默认值是1024k
通过jinfo -flag ThreadStackSize
查看可以得到-XX:ThreadStackSize=1024
可以通过-Xss指定栈的大小
-Xss1m
-Xss1024k
-Xss1048576
4.每个线程都有这样大小的一个栈吗?
每个Java虚拟机线程都有一个私有Java虚拟机堆栈,与该线程同时创建
5.JVM 栈中存储的是什么
保存局部变量和部分结果
6.Java 对象会不会分配到栈中?
JDK8的虚拟机文档中关于堆的描述是,所有类实例和数组的内存都是在堆中分配的
The heap is the run-time data area from which memory for all class instances and arrays is allocated.
逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态
如果经过逃逸分析后发现,对象并没有逃逸出方法,那么可以被优化成栈上分配。这样就无需在堆上分配内存,也无需进行垃圾回收
未逃逸对象可以进行
- 栈上分配
将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。 - 标量替换
标量
:是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。
有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分或全部可以不存储在内存,而是存储在CPU寄存器中
在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来替换。这个过程就是标量替换。 - 同步消除
如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步
只有在Server模式下,才可以启用逃逸分析。
逃逸分析目前并不成熟,其根本原因就是不能保证逃逸分析的性能收益必定高于它的消耗。因为逃逸分析本身就是一个高耗时的过程,假如分析的结果是没有几个不逃逸的对象,那么这个分析所花费时候比优化所减少的时间更长,这是得不偿失的。
由于HotSpot虚拟机目前的实现方式导致栈上分配实现起来比较复杂,因此在HotSpot中暂时还没有做这项优化。