一、运行时数据区总体架构图
1、红色部分为多线程共享的,灰色部分为单独线程私有的。
- 每个线程:独立包含程序计数器、栈、本地方法栈;
-
线程间共享:堆、堆外内存(永久代或元空间、代码缓存)
1.2 程序计数器
1.2.1 相关概念
1、作用:程序计数器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
2、在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,声明周期与线程的生命周期保持一致。
3、任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址:或者,如果实在执行native方法,则是未指定值。
4、字节码解释器工作时就是通过改变这个计数器的值来读取下一条需要执行的字节码指令。
1.2.2、代码演示:
public class PCRegisterTest {
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
}
}
通过编译
再通过javap指令进行反编译
javap -v PCRegisterTest.class
红色标注的就是指令地址(偏移地址)
1.2.3、两个常见问题
1)为什么使用程序计数器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪个位置开始继续执行。JVM的字节码解释器就需要通过改变程序计数器的值来明确一下条应该执行什么样的字节码指令。
2)程序计数器为什么会被设定为线程私有的?
为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个程序计数器,这样以来各个线程之间可以进行独立的计算,从而不会出现相互干扰的情况。
1.2.4 CPU时间片
CPU时间片即CPU分配给各个程序的时间,每个线程被分配一个时间段,称为它的时间片。
在宏观上:我们可以同时打开大哥应用程序,每个程序并行不悖,同时运行。
在微观上:由于只有一个CPU,一次只能处理程序要求的一部分。如何处理公平,一种方法就是引入时间片,每个程序轮流执行。
2.1 虚拟机栈
2.1.1 基本概念
1、栈是运行时的单位,而堆是存储的单位。
栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪里。
2、Java虚拟机栈是什么?
- Java虚拟机栈,每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧,对应着一次次的Java方法调用。一个栈帧就对应一个Java方法。
- 是线程私有的。
- 生命周期和线程一致。
-
作用:主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参入方法的调用和返回。
2.1.2 栈的特点
1、栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
2、JVM直接对Java栈的操作只有两个:
- 每个方法执行,伴随着进栈(入栈、压栈)
-
执行结束后的出栈工作
3、对于栈来说不存在垃圾回收问题
4、栈中可能出现的异常
2.1.3 设置栈内存大小
我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数用的最大可达深度。
1、演示栈内存溢出StackOverflowError的情况
/**
* @author aaa
* @date 2022-07-24 15:22
* @description StackOverflowError 11412
*/
public class StackErrorTest01 {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}
运行结果:
栈内存溢出,默认情况 count = 11412
2、Idea中设置栈空间大小
设置完栈空间大小后,再运行
发现 count=2458
2.1.4 栈的内存单元
1、栈中存储什么?
-
每个线程都有自己的栈,栈中的数据都是
以栈帧的格式存在
。 - 在这个线程上正在执行的每个方法都各自对应一个栈帧。
- 栈帧是一个内存区块,是一个数据集,维系这方法执行过程中的各种数据信息。
2、栈运行原理
2.1.5 栈帧的内部结构
每个栈帧中存储着:
-
局部变量表
-
操作数栈
- 动态链接(或指向运行时常量池的方法引用)
- 方法返回地址(或方法正常退出或异常退出的定义)
-
一些附加信息
2.1.5.1 局部变量表(local variables)
1、局部变量表基本概念
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。
当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
2、关于槽的理解
-
局部变量表的最基本的存储单元是Slot(变量槽)
-
局部变量表中存放编译器可知的各种基本数据类型(8种)、引用类型等。
3、通过Idea中的工具分析局部变量表
如果是非静态方法的普通方法,局部变量表索引0的位置存放的是this对象引用。
2.1.5.2 操作数栈
1、每个独立的栈帧中除了包含局部变量表外,还包含一个后进先出的操作数栈,也可以称之为表达式栈。
2、操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)/出栈(pop)。
3、操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
4、操作数栈
并非采用访问索引的方式来进行数据访问的
,而是只能通过标准的入栈(push)和出栈操作来完成一次数据访问。
5、代码追踪
public class PCRegisterTest {
public static void main(String[] args) {
int i = 15;
int j = 8;
int k = i + j;
}
}
分析:
1、首先是操作数栈是空的,程序计数器值为0
2、执行bipush操作入栈时,会先将15这个数值压入操作数栈中的栈顶位置。
3、程序计数器的值为1,向下执行,将操作数栈中的值15存放到局部变量表中,程序计数器的值再加1。数值8的存储过程和上面一样,都会将值存放到局部变量表中,不再赘述。
4、执行iload_1和iload_2指令时会将局部变量表中取出数据后并存放到操作数栈中,
5、接下来执行iadd操作,两个数再次出栈并通过执行引擎执行两数之和的操作,将得到的结果存储在操作数栈中。执行istore_3指令,将结果以int的方式存储在局部变量表中。
2.1.5.3 动态链接(指向运行时常量池的方法引用)
1、每一个栈帧内部都包含一个指向
运行时常量池
中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。
2、在字节码文件中有一个常量池
2.1.5.4 方法返回地址
1、存放调用该方法的PC寄存器的值。
2、一个方法的结束,有两种方式:
- 正常执行完成
-
出现未处理的异常,非正常退出
3、无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,
调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址
。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
2.1.5.5 栈相关面试题
何为线程安全?
- 如果只有一个线程才可以操作此数据,则是线程安全的。
- 如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。
3、什么是本地方法
一个Native Method就是一个Java调用非Java代码的接口。
使用native修饰的方法称为本地方法。
4、本地方法栈