谈谈你对虚拟机栈的理解
是什么? Java虚拟机栈(Java Virtual Machine Stack)是线程私有 的。每个线程拥有一个虚拟机栈,其内部保存一个一个的栈帧(Stack Frame) ,对应着一次一次的Java方法调用 ,即一个方法对应一个栈帧。
作用 主管Java程序的运行,它保存方法中的局部变量、部分结果,并参与方法的调用和返回。
栈的生命周期 栈的生命周期和线程一致,当线程结束了,该虚拟机栈也就销毁了。
和堆对比 栈是运行 时的单位,而堆是存储 的单位 栈解决程序的运行问题 ,即程序如何执行,如何处理数据。 堆解决的是数据存储的问题 ,即数据怎么放,放哪里
可能存在的异常问题 对于栈来说不存在垃圾回收问题 ,因为出栈即相当于回收,但是存在栈溢出 的情况
Java栈的大小 分动态或固定不变两种
Java的指令根据栈来设计 由于跨平台性的设计,不同平台CPU架构不同,不适合设计成基于寄存器,所以Java的指令都是根据栈来设计的。 优点 是跨平台,指令集小,编译器容易实现, 缺点 是性能下降,实现同样的功能需要更多的指令。
栈帧
什么是栈帧(Stack Frame)
栈帧是栈的基本存储单位,栈中的数据都是以栈帧 的格式存在。一个方法对应一个栈帧。
栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
不同线程的栈帧不允许相互引用 即不可能在一个线程的栈帧之中引用 另外一个线程的栈帧。
当前栈帧
在一个线程中,同一时间只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧是有效的 这个栈帧被称为当前栈帧(Current Frame) 与当前栈帧相对应的方法就是当前方法(Current Method) 定义这个方法的类就是当前类(Current Class)
执行引擎运行的所有字节码指令 只针对当前栈帧 进行操作。
如果在当前方法中调用了新的方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。 例如:如果当前方法A调用了其他方法B,则B成为当前方法,方法B返回之际,当前栈帧B会传回此方法的执行结果给前一个栈帧A,接着,虚拟机会丢弃当前栈帧B,使得前一个栈帧A重新成为当前栈帧 。 图示:
栈帧的内部结构
每个栈帧中包括:
- 局部变量表(Local Variables,本地变量表)
- 操作数栈(operand Stack,表达式栈)
- 动态链接(DynamicLinking,指向运行时常量池的方法引用)
- 方法返回地址(Return Address,方法正常退出或者异常退出的定义)
- 一些附加信息
栈帧的大小主要由局部变量表 和 操作数栈 共同决定的
图示: 下面一一详细介绍这些内部结构
局部变量表
是什么? 主要用于存储方法参数 和定义在方法体内的局部变量 (包括8种基本数据类型、对象引用(reference),以及return Address类型) 在方法执行时,虚拟机使用局部变量表来完成方法参数和局部变量 的传递。
特点 1、由于局部变量表是栈帧的内部结构,所以是线程的私有数据,因此不存在数据安全问题
2、局部变量表的大小在编译期 就确定下来了,在方法运行期间不会改变。
3、方法嵌套调用的次数由栈的大小和方法含有的数据量 共同决定。 一般来说,栈越大,方法嵌套调用次数越多。 方法包含的参数和局部变量越多,栈帧就越大,嵌套调用次数越少。
4、局部变量表中的变量只在当前方法调用中有效
5、在栈帧中,与性能调优关系最为密切的部分就是局部变量表 。
6、局部变量表中的变量 也是重要的垃圾回收根节点 ,只要被局部变量表中直接或间接引用的对象都不会被回收。
7、局部变量表的生命周期 和栈帧一致
Slot
什么是Slot? Slot(变量槽) 是局部变量表最基本的存储单元 ,一个局部变量表由N个slot 组成 在局部变量表里,32位以内的类型(char,int)只占用一个slot,64位的类型(long和double)占用两个slot
栈——栈帧——局部变量表——Slot
slot的工作原理: JVM会为局部变量表中的每一个Slot 都分配一个访问索引 ,通过这个索引即可成功访问到局部变量表 中指定的局部变量值 index=0 ,也就是第一个索引处放对象引用this 。当一个实例方法被调用的时候,它的方法参数 和方法体内部定义的局部变量 将会按照顺序 依次被复制到局部变量表中的每一个slot上。
Slot的重复利用 局部变量表中的slot槽位是可以重用的 举例: 下面方法localVar2中的局部变量a出了代码块,也就过了其作用域之后,就会被销毁,但是其槽位不会销毁,那么在其作用域之后申明的新的局部变量b 就很有可能会复用过期局部变量a的槽位,从而达到节省资源的目的。
操作数栈Operand Stack
作用: 主要用于保存计算过程的中间结果 ,同时作为计算过程中变量临时的存储空间
工作机理: 操作数栈在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop) 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈,比如:执行复制、交换、求和等操作
动态链接
是什么?
为什么需要运行时常量池?直接将方法存放在栈中不行吗?何必多此一举?———为了节约内存空间
在运行时常量池 中存储一份数据(张三本人),每次栈中的方法需要张三时就用“张三” 指向运行时常量池来获取张三本人 ,
否则每个栈中的方法都要存一份张三本人 ,太浪费空间了。
方法返回地址
一个方法的结束,有两种方式: 1、正常执行完成并退出 2、出现未处理的异常时非正常退出
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置,即方法返回地址
一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
虚方法和非虚方法
如果方法在编译期 就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法 ——静态 要等到运行期才能确定版本的为虚方法 ——动态
非虚方法 包括:静态方法、私有方法、final方法、实例构造器、父类方法 其他方法称为虚方法
方法的调用:虚方法表
为了提高性能,JVM采用在类的方法区建立一个虚方法表 表中存放虚方法 的调用版本。 非虚方法 不会出现在表中,因为非虚方法在编译时就能确定其调用版本。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口
虚方法表是什么时候被创建的呢?
虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的虚方法表也初始化完毕。 如上图所示:如果类中重写了方法,那么调用的时候,就会直接在自己的虚方法表 中去找。否则将会直接连接到Object的方法中。
面试题
调整栈大小,就能保证不出现溢出么? 不能保证不溢出。只能延长
分配的栈内存越大越好么? 不是,内存大确实可以降低OOM概率,但是会挤占其它的线程空间,因为整个空间是有限的。
垃圾回收是否涉及到虚拟机栈? 没有
方法中定义的局部变量是否线程安全?
下面举例说明是否安全(认真看)
public class StringBuilderTest {
public static void method01() {
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
public static StringBuilder method04() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder;
}
public static void method02(StringBuilder stringBuilder) {
stringBuilder.append("a");
stringBuilder.append("b");
}
public static void method03() {
StringBuilder stringBuilder = new StringBuilder();
new Thread(() -> {
stringBuilder.append("a");
stringBuilder.append("b");
}, "t1").start();
method02(stringBuilder);
}
public static String method05() {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("a");
stringBuilder.append("b");
return stringBuilder.toString();
}
}
总结:如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。
运行时数据区哪些存在Error和GC?
运行时数据区 | 是否存在Error | 是否存在GC |
---|
程序计数器 | 否 | 否 | 虚拟机栈 | 是 | 否 | 本地方法栈 | 是 | 否 | 方法区 | 是(OOM) | 是 | 堆 | 是 | 是 |
补充
栈顶缓存技术Top Of Stack Cashing
基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派次数和内存读/写次数 。由于操作数是存储在内存中的,因此更多的指令分派次数和内存读/写次数 必然会影响执行速度。
为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存技术 ,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
字节码中方法内部结构剖析
main方法:
可以看到下图中
Name: main
Desciptor: Ljava/lang/String 中开头的L表示引用类型变量,总共表示一维的String数组。
Access flags:访问标识(public static)
一共就完整描述了 public static void(String[] args)
下面就是code的介绍:
下面是LineNumberTable的介绍:
下面是对LocalVariableTable的描述
|