首先上图
JVM运行时数据区包括:PC寄存器,JVM栈,本地方法栈,堆,方法区几个部分
1. PC寄存器
Program Counter,程序计数器
唯一一个在JVM规范中没有规定OOM情况的区域
没有GC
访问速度最快
记录指令地址,便于线程的上下切换
是线程私有的,每个线程都分配了一个PC寄存器,PC寄存器的生命周期与线程的生命周期保持一致
2. 虚拟机栈
每个线程创建的时候都会创建一个虚拟机栈,其内部保存着一个个的栈帧(Stack Frame),对应着一次次的Java方法调用,是线程私有的
JVM栈中保存:局部变量(基本数据类型的值+引用数据类型的引用),部分结果,方法的调用和返回
栈的访问速度仅次于PC
对于栈来说,不存在GC,但是有可能会OOM
栈中可能的异常:
StackOverflowError
:采用固定大小的虚拟机栈,线程请求分配的栈容量超过java虚拟机栈允许的最大容量OutOfMemeryError
:采用动态扩展的虚拟机栈,线程尝试扩展时无法申请到足够的内存,或者线程创建时没有足够内存去创建对应的虚拟机栈- 设置栈的大小:
-Xss256k
设置线程的最大栈空间,决定了函数调用的最大可达深度
栈帧
JVM栈中的数据都是以栈帧(Stack Frame)的格式存在,线程上正在执行的每个方法都各自对应一个栈帧
不同线程所包含的栈帧不可以相互引用
栈帧的结构:
-
- 局部变量表
- 操作数栈(或表达式)
- 动态链接(或指向运行时常量池的方法引用)
- 方法返回地址(或方法正常退出或异常退出的定义)
- 一些附加信息
局部变量表
也称为局部变量数组,或者本地变量表。定义为一个数字数组,存储方法参数和方法体内的局部变量,数据的类型是基本数据类型,对象的引用和 returnAddress类型。
局部变量表的基本存储单元是Slot,32位以下的类型占用一个slot(也包括returnAdderss),64位类型占用两个slot(long和double),另外byte、short、char都被转换为int存储,boolean也被转换为int 0,1
JVM为每个Slot分配了访问索引,方法调用时,方法参数和局部变量按声明顺序被复制到局部变量表的每个Slot
如果当前帧是构造方法或者实例方法,Slot的index0会存放该对象的引用this,其余参数按顺序继续存放。故而静态方法不能用this,因为栈帧中根本就没有这个变量
Slot可以重复利用: 当局部变量过了作用域后,重新声明的新局部变量就可以复用过期局部变量的槽位,节省资源
局部变量表的大小在编译期就确定了,保存在code属性的maximum local variables数据项中
当方法调用结束后,随着方法栈帧的销毁,局部变量表也随之销毁
局部变量表中的变量是重要的垃圾回收根节点(GC ROOTS),被局部变量表中直接或间接引用的对象不会被回收
操作数栈
操作数栈
用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈出栈。某些指令将值压栈,由其他指令将操作数取出栈,运算后再把结果压栈
操作数栈的深度在编译期就确定好了,保存在Code
属性的max_stack
数据项中
和slot类似,32bit的类型占用一个栈深度,64bit的类型占用两个栈深度
对于byte
范围内的数,直接按bipush
单字节入栈,short
类型就按sipush
双字节入栈,但是存入局部变量表中都是int了
由于JVM操作码是一个字节的零地址指令,这意味着指令集的操作码总数不可能超过256条,大部分指令都没有支持 byte、char 和 short 类型,甚至没有任何指令支持 boolean 类型。编译器会在编译器或运行期将 byte 和 short 类型带符号扩展为 int 类型, boolean 和 char 类型零位扩展为相应的 int 类型, 因此,大多数对于 boolean、byte、char 和 short 类型数据的操作,实际都是使用 int 类型作为运算类型
方法的调用
JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关
静态链接:字节码文件装载进JVM,如果被调用的目标方法在编译期可知,且运行期保持不变,这时将调用方法的符号引用转换为直接引用的过程就是静态链接
动态链接:被调用方法在编译期无法被确定下来,也就是只能在运行期将调用方法的符号引用转换为直接引用,这种引用转换过程称为动态链接
面向对象的语言都具备多态特性,具备早期绑定和晚期绑定两种方式
- 非虚方法:编译期就确定具体调用版本,该版本运行时不可改变:
静态方法,私有方法,final方法,实例构造器,父类方法
- 虚方法:其他在运行时可以体现多态特性的方法
- JVM提供了几种调用方法的指令:
invokestatic
:非虚方法。调用静态方法,解析阶段确定唯一方法版本invokespecial
:非虚方法。调用<init>
方法、私有方法
和父类方法
,解析阶段确定唯一方法版本invokevirtual
:调用所有虚方法,final修饰的方法也会用invokevirtual,除此之外都是虚方法invokeinterface
:调用接口方法invokedynamic
:动态解析出需要调用的方法,然后执行。java8的lambda表达式就是通过该指令调用
面向对象由于多态特性,会频繁的使用动态分派,如果每次动态分派都要重新在类的方法元数据中搜索到合适的目标的话就会影响到执行效率。 因此为了提高性能,JVM在类的方法区建立了虚方法表
,表中存放着各个方法的实际入口,使用索引表来替代查找
方法返回地址
存放该方法的调用者的pc寄存器的值
方法正常退出时,调用者的pc寄存器的值作为返回地址,这样就可以返回到该方法被调用的位置继续执行
方法异常退出时,返回地址通过异常表来确定,栈帧中一般不会保存这部分信息
字节码指令中,返回指令包含 ireturn(boolean-int), lreturn, freturn, dreturn, areturn(引用类型), return(void方法,构造方法)
3. 本地方法栈
native方法
就是Java调用非Java代码的接口。本地接口的作用是融合不同的语言为java所用
Java虚拟机栈
用于管理Java方法的调用,而本地方法栈
用于管理本地方法的调用
本地方法栈也是线程私有的
可以设计成固定大小或者可扩展大小。内存溢出方面和java虚拟机栈的情况是一样的,同样会出现StackOverFlowError和OutOfMemoryError
当线程调用一个本地方法时,他就进入了一个全新的不再受虚拟机限制的世界,它和虚拟机拥有同样的权限!
- 本地方法可以通过本地方法接口来
访问虚拟机内部的运行时数据区
- 可以直接使用本地处理器中的寄存器
- 可以直接从本地内存的堆中分配任意数量的内存
并不是所有的java虚拟机都支持本地方法栈
HotSpot虚拟机中,直接将本地方法栈和虚拟机栈合二为一了