程序计数器
每个线程都有自己的程序计数器(线程私有),它可以看作是当前线程所执行的字节码的行号指示器。
在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令。
为什么程序计数器线程私有
主要是为了保证进程切换之后能够恢复到正确的执行位置。
Java方法 & 本地方法
如果执行的是Java方法,那么程序计数器记录的是正在执行的虚拟机字节码。
如果执行的是本地方法(C++方法),那么值为空。
虚拟机栈
由于跨平台性,Java的指令都是根据栈来设计。
优点:不依赖硬件,跨平台,指令集小,编译器容易实现。
缺点:性能下降,实现同样的功能需要更多的指令。
虚拟机栈解决了程序的运行问题,堆解决了数据的存储问题。
概念
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,每调用一个方法,相应的就会生成该方法的栈帧。
生命周期
其生命周期和线程一样。
作用
主管Java程序的运行,它保存方法的局部变量、部分结果并参与方法的调用和结果返回。
栈的优点
-
操作方便,只需要两个操作,入栈和出栈
-
不存在垃圾回收问题
-
栈是一种快速有效的分配存储方式,访问速度仅次于PC寄存器。
StackOverFlowError & OutOfMemoryError
如果线程所请求的栈深度大于虚拟机所允许的深度时
1、若支持虚拟机栈扩展,那么拓展到没内存时到最后就会报出OutOfMemoryError
2、若不支持扩展,那么会报出StackOverFlowError
StackOverFlowError发生时,整个程序会崩溃吗
SpringBoot采用了线程隔离机制,如果发生StackOverFlowError的线程是独立的、非关键的,可能不会造成程序的崩溃。
栈的存储单位-栈帧
栈中的数据都是以栈帧的格式存在,每个方法都各自对应着一个栈帧。
我们通常把虚拟栈的栈顶元素称为当前栈帧,与其对应的方法称为当前方法,定义这个方法的类就是当前类。
栈帧的出栈
栈帧出栈有两种可能:
-
正常的函数返回,使用return指令
-
抛出异常
栈帧的内部结构
每个栈帧都存储着:
-
局部变量表
-
操作数栈(或表达式栈)
-
动态链接(或指向运行时常量池的方法引用)
局部变量表
也被称为局部变量数组或本地变量表。
定义
一个用来存储方法参数和方法体内局部变量的数字数组。数据类型包括各类基本数据类型、对象引用以及returnAddress类型。
相关知识
-
局部变量表建立在线程之上,所以不存在数据安全问题。
-
局部变量表所需要的容量大小是在编译期确定下来的。
-
局部变量表中的变量只在当前方法调用中有效。随着方法栈帧的销毁而销毁。
-
方法嵌套调用的次数由栈的大小决定。
局部变量表的存储单元-Slot(变量槽)
-
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
- byte、short、char在存储前被转换为int;boolean也被转换为int,0表示为false,非0表示为true。
-
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。
-
JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
-
如果需要访问局部变量表中的一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double)
-
如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。
-
局部变量表中的slot是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
public void test(){
{
int a = 0;
}
int b = 0;
}
// 此时b会复用a的槽位
- 局部变量表中的变量是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
操作数栈
定义
也被称为表达式栈,具有先进后出的特性。
作用
-
算术操作
-
方法调用:调用一个方法时,先将方法的参数压入操作数栈,然后执行调用指令,方法的返回值会压入操作数栈。
-
条件判断
总结:指令执行的临时存储、方法参数和返回值的传递、数据的加载和存储
相关知识
-
主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
-
操作数栈是JVM执行引擎的工作区,当一个方法刚开始执行时会创造一个新的栈帧,这个新栈帧的操作数栈是空的。
-
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就定义好了,保存在方法的Code属性中,为max_stack的值。
-
栈中的任意一个元素都是Java数据类型。
-
32bit的占用一个栈单位深度
-
64bit的占用两个栈单位深度
-
-
操作数栈用出栈、入栈来完成数据访问。
-
如果被调用方法带有返回值,那么会将返回值压入当前栈帧中的操作数栈。
栈顶缓存技术
技术解决的问题
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。
定义
将栈顶元素全部缓存到CPU的物理寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。
动态链接
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
动态链接的作用:为了将这些符号转换为调用方法的直接引用。
常量池的作用:为了提供一些符号和常量,便于指令的识别。
方法的调用
在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。
-
静态链接(早期绑定)
被调用的目标方法在编译器可知,且运行期保持不变。
-
动态链接(晚期绑定)
被调用的目标方法在编译器确定不下来,要在程序运行期将调用方法的符号引用转换为直接引用,这种引用转换过程具备动态性。
非虚方法与虚方法
-
非虚方法在编译期就确定,在运行时不可变。
-
静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
-
其他方法称为虚方法,其在运行时确定。
虚方法表
为了避免每次动态分派都要重新在类的方法元数据中搜索,因此JVM在类的方法区创建了虚方法表,每次在虚方法表查找即可。
虚方法的创建时机:在类加载的链接阶段被创建并初始化。
Java语言是静态类型语言,在Lamda表达式出现之后,有了一些动态类型语言的特性。
静态类型语言与动态类型语言
-
静态类型语言是判断变量自身的类型信息
-
动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息。
-
区别:静态类型语言在编译期就对类型进行检查,动态类型语言在运行期才对类型进行检查。
方法返回地址
方法返回地址:存放调用该方法的PC寄存器的值。
方法正常退出时:调用者的PC寄存器的值作为返回地址,即调用该方法的指令的下一条地址。
方法异常退出时:返回地址通过异常表来确认,栈帧一般不会保存此信息。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上一层任何返回值。
本地方法栈
和虚拟机栈一样。
区别就是:虚拟机栈内存放的是Java方法的栈帧,本地方法栈内存放的是本地方法的栈帧。
所以这里就不过多赘述。
堆
Java堆是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块区域。
几乎所有的对象实例都分配在这里。
TLAB(Thread Local Allocation Buffer)
原因
堆区是线程共享的区域,因为是共享的,在多个线程下,就会存在线程安全问题。所以就出现了TLAB来解决这个问题。
定义
从内存模型而不是垃圾回收的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden内。
多线程同时分配内存时,使用TLAB能够解决线程安全问题,同时也能提升内存分配的吞吐量,我们将这种内存分配方式称为快速分配策略。
一旦对象在TLAB空间分配内存失败时,JVM就会尝试使用加锁机制来确保数据的原子性。
方法区
尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾回收或者压缩。方法区看作是一块独立于Java堆的内存空间。
永生代 & 元空间
JDK7之前把方法区叫永生代,JDK8之后把方法区叫元空间。
元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是在本地内存。如果方法区无法满足新的内存分配要求,将抛出OOM异常。放在本地内存的话,能够保证内存比原先的虚拟机设置的内存多。
方法区的内部结构
方法区的内部存储内容:类型信息、常量、静态变量、即时编译器编译后的代码缓存。
类型信息
对每个加载的类型(类Class、接口Interface、枚举enum、注解annotation),JVM必须在方法区中存储如下信息:
-
这个类型的完整有效名称(全名=包名+类名)
-
这个类型直接父类的完整有效名(接口和Object没有)
-
这个类型的修饰符
-
这个类型直接接口的一个有序列表
域(Field)信息
域相关信息:域名称、域类型、域修饰符
方法(Method)信息
方法相关:
-
方法名称
-
方法返回的类型
-
方法的参数的数量和类型
-
方法的修饰符
-
方法的字节码、操作数栈、局部变量表及大小
-
异常表