JVM之运行时数据区 PC、虚拟机栈、本地方法栈
- PC寄存器
- 线程回顾
- 寄存器
- 实践
- 面试
- 使用PC寄存器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址
- PC寄存器为什么会被设定为线程私有
- 虚拟机栈
- 虚拟机栈出现背景
- 简介
- 栈可能出现的异常
- 栈中存储着什么
- 运行原理
- 栈帧内部结构
- 局部变量表
- Slot
- 操作数栈
- 栈顶缓存技术
- 动态连接(指向运行时的方法引用)
- 方法调用
- 虚方法与非虚方法
- 方法返回地址
- 一些附加信息
- Error & GC
- 本地方法栈
PC寄存器
线程回顾
- 线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行的执行
- 在HotSpot JVM里,每个线程都与操作系统的本地线程直接映射
当一个Java线程准备好执行以后,此时一个操作系统的本地线程同时创建。Java线程执行终止后,本地线程也会回收 - 操作系统负责所有线程的安排调度到任何一个可用的CPU上。一旦本地线程初始化成功,他就会调用JAVA线程中的run方法
- 如果你使用jconsole或者任何一个调试工具,都能看到在后台许多线程在运行。这些后台线程不包括调用public static void main(String[])的main线程以及所有这个main线程自己调用的线程
- 这些主要的后台系统线程在HotspotJVM里主要是以下几个
虚拟机线程:这种现成的操作需要JVM达到安全点才会出现。这些操作必须在不同的线程中发生的原因使他们都需要JVM达到安全点,这样堆才不会变化。这种线程的执行类型包括“stop-the-world”的垃圾收集,线程栈收集,线程挂起以及偏向锁撤销
周期任务线程:这种线程是时间周期事件的体现(比如中断),他们一般用于周期性操作的调度执行
GC线程:这种线程对JVM里不同种类的垃圾收集行为提供了支持
编译线程:这种现成运行时会将字节码编译成到本地代码
信号调度线程:这种接收信号并发送給JVM,在它内部通过调用适当的方法进行处理
寄存器
JVM中的程序计数器(Program Counter Register)中,Register 的命名源于CPU的寄存器,寄存器存储至指令相关的现场信息。CPU只有数据装载到寄存器才能够运行。
这里,并非是广义上所指的物理寄存器,或者将其翻译为PC计数器会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象
- 他是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
- 在JVM规范中,每个线程都有自己的程序计数器,是线程私有的,生命周期与线程的生命周期一致
- 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址:或者,如果是在执行native方法,则是未指定值(undefined)
理解:每个线程都有一个寄存器,寄存器记录了当前线程执行代码指令的顺序
实践
class文件反编译指令:javap -verbose file.class 或者 javap -v file.class
面试
使用PC寄存器存储字节码指令地址有什么用?为什么使用PC寄存器记录当前线程的执行地址
因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令
总结:通过指令地址找到操作指令,记录下一步指令如何执行
PC寄存器为什么会被设定为线程私有
我们知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停的做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然是每个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从不会出现相互干扰的情况
由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或多核处理器中的一个内核,只会执行某个县城中的一条指令.
这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响
虚拟机栈
虚拟机栈出现背景
由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的
优点是 跨平台,指令集小,编译器容易实现,缺点是 性能下降,实现同样的功能需要更多的指令集
简介
-
java虚拟机栈是什么
java虚拟机栈,早起叫做java栈,
每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应这一次次的Java方法调用。
线程使用 -
生命周期
生命周期和线程一致 -
作用
主管java程序的运行,他保存方法的局部变量、部分结果,并参与方法的调用和返回
栈可能出现的异常
java虚拟机规范允许Java栈的大小是动态的或者固定不变的
- 如果采用固定大小的java虚拟机栈,那每一个线程的java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常
- 如果java虚拟机可以动态扩展,并且尝试扩展的时候无法申请到足够的内存,或者创建新的线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个OutOfMemoryError异常
修改栈内存大小
public class Test {
private static int i = 1;
public static void main(String[] args) {
i++;
System.out.println(i); // 未修改时 : 11405 修改后 2471
main(args);
}
}
栈中存储着什么
- 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在
- 在这个线程上正在执行的每隔方法都各自对应一个栈帧(Stack Frame)
- 栈帧是一个内存块,是一个数据集,维系着方法执行过程中得各种数据信息
运行原理
- 操作:压栈 出栈,遵循“先进先出”原则
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈相对应的方法就是 当前方法(Current Method),定义这个方法的类就是当前类(Current Class)
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作
- 如果该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧
栈帧内部结构
局部变量表
- 定义为 一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference)、以及returnAddress类型
- 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximun local variables数据项中。在方法运行期间是不会改变局部变量表的
Slot
操作数栈
栈顶缓存技术
动态连接(指向运行时的方法引用)
- 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。
包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking),比如:invokedynamic指令 - 在Java源文件被编译到字节码文件时,所有的变量和方法引用都作为符合引用(Symbolic Reference)保存到class文件的常量池里。
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法引用来表示的,那么 动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
为什么要常量池?
作用:提供一些符号和常量,便于指令识别
方法调用
动态链接、晚期绑定 例子:多态
虚方法与非虚方法
方法返回地址
一些附加信息
jvm不同实现而言,可选
Error & GC
运行数据区 | Error | GC |
---|---|---|
pc | X | X |
栈 | √ | X |
堆 | √ | √ |
方法区 | √ | √ |
本地方法栈