一、运行时数据区的结构
一个有意思的比喻,执行引擎相当于厨师,运行时数据区相当于做饭的材料去,厨师用什么拿什么,做完饭之后需要收拾(GC)。
看下面的图:
红色部分是同进程声明周期(JVM),灰色区域是同线程声明周期,所以如果村存在5个线程,那么就有5份程序计数器、本地方法栈、虚拟机栈,他们共享一份方法区、堆。针对JVM优化主要就是针对方法区和堆,因为大部分都是垃圾回收都是在堆中进行,JDK8之后,方法区也叫元空间使用的是本地内存,一般情况下不会溢出。
一个JVM对应一个Runtime实例,一个Runtime实例,对应一份运行时数据区。
二、程序计数器(PC 寄存器\程序计数器)
记录程序执行到哪里,也可称为程序钩子。
用来存储存储指向下一条指令的地址,就是将要执行的指令,由执行引擎读取下一条指令。有点类似于数据库的游标或者集合的迭代器。
没有GC、也不会发生OOM。
举例:
常见的问题:
- 为什么使用PC寄存器记录当前线程执行的地址呢?CPU切换线程后,需要知道继续从哪里执行
- PC寄存器为什么线程私有的?如果不是私有的,记录的就乱了
三、虚拟机栈
出现背景,优点是跨平台(相对于寄存器结构),指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
在JVM中,栈是运行时单位,堆是存储单位。
下面的图,左边是堆、右边是栈(感谢老师):
每个线程创建的时候都会创建一个虚拟机栈,其内部保存着一个个的栈帧(Stack Frame),对应着一次次方法的调用。主管java程序的运行,保存局部变量、部分结果,并参与方法的调用和返回。主要是基本类型的局部变量,其他的局部变量只存放引用,实际存放在堆中。
对于栈来说不存在GC,但是存在OOM。
常见异常和设置大小
异常:
- StackOverflowError:大小为固定值的时候,超过栈的大小(main自己调用自己)
- OutOfMemoryError:大小为动态扩展,在申请的时候没有足够的内存
-Xss:参数来设置栈空间的大小(所以在设置TOMCAT参数的时候,这个不必太大)
执行的方法和栈中的栈帧一一对应的,在一个时间点上只有一个活动的栈帧。
各个栈之间的栈帧是无法相互引用的,也就是线程之间的数据相互之间是不可见的。栈帧的结束,分为2中,一种为 return,一种为抛出异常。
栈帧内部结构
- LV:局部变量表
- OS:操作数栈
- DL:动态链接
- RA:方法返回地址
- 一些附加信息
1、局部变量表
是一个数字数组,方法的参数、局部变量(基本数据类型、对象引用)、返回值类型。大小编译的时候就确定下来,不会改变。主要影响栈帧的大小。
代码和字节码对应的关系:
因为是在一个线程中,所以不会存在线程安全问题。
关于Slot的理解
局部变量表的基本单位,就是Slot,32以内的占用一个Slot,64类型(long和double)占用2个Slot。
变量按照声明的顺序放入,构造方法和非静态方法都有一个this(index=0)变量,也会放入到局部变量表中。
当一个变量出了自己的作用于,Slot就会回收,其他变量就会继续使用Slot。
因为在栈帧中,局部变量表示主要存储,所以调优也在这里,例如当局部变量表中一个引用销毁,对应的堆中的对象也要销毁(可能)。局部变量表也是GC的根节点,只要被局部变量中直接或间接引用的对象都不会被回收。
2、操作数栈(表达式栈)
用数组实现栈结构
- 主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间
- 栈帧创建出来的时候,操作数栈是空的(没有计算当然是空),但是因为是数组所以大小是固定的。保存在max_statck中
- 32位一个深度、64位两个深度
- 访问的时候,必须准守栈的规则
栈顶缓存技术(Top-of-StackCashing):
由于栈式架构的特点,指令虽然小,但是执行一个指令的操作变得更加长,效率变低,将栈顶的元素全部缓存在CPU的寄存器中,降低对内存的读写,从而提升效率。
3、动态链接
每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
字节码中的常量池,存放这个类中的方法、变量的引用都作为符号引用,保存在字节码的常量池中,比如:描述一个方法调用了其他方法是,就是通过常量池中指向方法的符号引用来标识的,动态链接就是为了将这些符号引用转换为方法的直接引用。
下面#27就是符号引用,需要转换成实际引用
方法的调用
在JVM中,将符号引用转换成方法的直接引用于方法的绑定机制有关:
- 静态链接/早起绑定:当一个字节码文件被加载到JVM中,如果被调用的目标方法在编译期可知,切运行期间保持不变,这种情况下降调用方法的符号转换为直接引用成为静态链接
- 动态链接/晚期绑定:如果被调用方法在编译期无法被确定下来,也就说只能在运行期将地阿偶用方法的符号引用转换为直接引用,叫做动态链接(体现了多态)
虚方法和非虚方法
- 虚方法:在编译器无法确定背调用的方法
- 非虚方法:在编译期间就确定了被调用的集体方法,并且不会在运行期间改变的
- 静态方法、私有方法、final方法、构造器、父类方法
动态类型语言和静态类型语言
类型是在编译期间就确定的就是静态类型语言,java就是。反之就是动态类型,js就是。
虚方法表
子类执行某个方法的时候,如果没有到父类寻找,一直到找到,但是这样效率太低了,所以建立一个虚方法表,直接指向实现了该方法的父类。
因为非虚方法不会被重写,所以不需要存在。
4、方法返回地址
存放调用该方法的PC寄存器的值,下一条需要执行的指令,因为方法退出了,都要回到调用该方法的地方继续执行(正常执行,如果一场直接抛出)。
5、一些附加信息
可能存在一些和虚拟机一些相关的信息。
四、一些面试题
- 举例栈溢出的情况
- 固定大小 StackOverFlowError
- -Xss ,可能出现OOM
- 调整栈的大小,就能不保证栈不出现溢出吗
- 无法一定保证不易出,因为运行代码的情况不确定
- 分配的栈内存越大越好吗
- 还是要根据实际情况去分配,太大了就会影响其他程序使用内存
- 垃圾回收是否涉及到虚拟机栈
- 不能,因为GC不会回收虚拟机栈(GC只有方法区、堆)
- 方法中的局部变量是否线程安全
- 具体问题具体分析,StringBuilder是线程不安全的,当定义成局部变量的时候,没有给传递给其他方法,那么他就是线程安全的,但是传递给其他方法了,就不安全了。内存产生,内部消亡的就安全了。
五、本地方法接口\库
就是Native Method,一个java方法调用非java的接口,例如在java中调用c的接口,就是为了让java能够融合c或者c++的特性。
使用native关键字进行修饰:
六、本地方法栈
管理本地方法的调用,也就是管理Native方法的调用。其相关特性同虚拟机栈相同。
虚拟机可以支持本地方法也可以不支持。
HotSpot 虚拟机把本地方法栈和虚拟机栈合二为一。