接上篇文章程序计数器继续讲解。
写在最前,本篇文章大部分来源于b站尚硅谷JVM全套教程的提炼,并附带自己的理解。主要是为了帮助自己理解,和用于复习。如果同时还能对其他人有所裨益,那就更好不过了。如果有谬误的地方,还请不吝指出。
背景
由于跨平台的设计,Java指令都是栈来设计的,不同平台CPU架构不同,所以不能设计为基于寄存器的。
优点:跨平台、指令集小、编译器容易实现
缺点:性能下滑、实现同样功能需要更多指令
栈是运行时的单位,而堆是存储的单位
即:栈解决程序运行的问题,比如程序如何执行,或者如何处理数据等。
而堆解决的是数据存储的问题,比如怎么放,放在哪。(实际上,栈也能存放一些数据,比如:基本类型变量,和对象的引用)
基本内容
java虚拟机栈是什么?
每个线程在创建时都会创建一个虚拟机栈,内部保存栈帧(栈存储数据的基本单位),对应java方法调用。线程私有。
生命周期:与线程一致
作用:主管java程序的运行,保存方法的局部变量、部分结果,并参与方法的调用和返回。
一个栈帧对应一个方法,栈顶方法即是当前方法。
特点:
- 快速有效,访问速度仅次于程序计数器
- 操作仅有两个:方法执行,压栈(进栈);执行结束,出栈。
- 不存在垃圾回收的问题。(但存在OOM问题)
**面试题:**开发中遇到的异常?
内存溢出OOM,StackOverFlow异常等
JVM规范允许java栈的大小是动态的,或者是固定不变的。
如果是固定的,那么每一个线程的java虚拟机栈容量可以在线程创建时独立选定。如果请求分配的栈容量超过允许的最大容量,则会抛出StackOverFlow异常。(允许的容量可能只是内存的一部分)
如果java虚拟机栈可以动态扩展,并且尝试扩展时无法申请到足够内存,或者创建新线程时,没有足够内存创建虚拟机栈,则会抛出OOM异常
通过参数-Xss可以调节线程的最大栈空间,oracle官方查找指令网址
-Xss size
Sets the thread stack size (in bytes). Append the letter k or K to indicate KB, m or M to indicate MB, and g or G to indicate GB.
栈的存储单位
方法和栈帧是一对一的关系:方法执行对应入栈,执行结束对应出栈。
栈帧是一个内存区块,是一个数据集。
一个活动线程中。在一个时间点上,只会出现一个活动的栈帧,即栈顶栈帧,被称为当前栈帧;对应方法:当前方法;定义这个方法的类:当前类。
执行引擎运行的所有字节码指令只针对当前栈帧操作。
如果在该方法中调用了其他方法,对应的新栈帧会被创建出来,放在栈的顶端,成为新的当前栈。
栈运行原理
不同线程包含的栈帧不允许存在相互引用,即不可能在一个栈帧之中引用另一个线程的栈帧。
如果当前方法调用了其他方法,方法返回时,当前栈帧会传回此方法的执行结果传给前一个栈帧,接着,JVM丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
两种返回函数的方式:return指令;抛出异常。都会导致栈帧被弹出。
栈帧的内部结构
每个栈帧存储:
- 局部变量表 (Local Variable)
- 操作数栈(Operand Stack)
- 动态链接 (Dynamic Linking)(或指向运行时常量池的方法引用)
- 方法返回地址 (Return Address)
- 一些附加信息
见下图:
面试问题
- 举例栈溢出的情况(StackOverFlow)
原因就是栈帧的数量超过了栈被设置的所能容纳的最大容量。
可以通过-Xss设置栈的大小。如果整个JVM的内存都不够用了,则会出现OOM异常。 - 调整栈大小,可以保证不溢出吗? 不能
- 分配的栈内存越大越好么? 会挤占其它线程的空间
- 方法中定义的局部变量是否线程安全?
分析:方法内部自身定义的局部变量线程安全(不发布的前提下):每个线程私有虚拟机栈,每个栈帧对应一个方法,里面会生成一个局部变量表,是线程私有的。这个方法不会被其他线程共享,所以如果其他线程也调用这个方法,是在它线程内部的虚拟机栈重新生成的栈帧,和原线程的栈帧除了都拥有指向同一个方法的引用外,没有其它相同点。所以一定是线程安全的。
如果是方法参数,则不一定是线程安全的。
如果发布(即将本地变量暴露出此方法之外),则不一定是线程安全的,因为不知道方法的调用者对此对象进行的操作。
如果变量本身就线程安全(如不可变对象,或线程安全类),或者变量为基本数据类型,则无论是作为方法参数,或是返回值,都是线程安全的。 - 垃圾回收会涉及到虚拟机栈么? 不涉及
GC | Error | |
---|---|---|
程序计数器 | X | X |
虚拟机栈 | X | √ |
本地方法栈 | X | √ |
方法区 | √ | √ |
堆 | √ | √ |