运行时数据区主要有五个部分:方法区;堆;程序计数器;本地方法栈;虚拟机栈
java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁,另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。
有的数据区是单线程私有的,包括程序计数器、栈、本地栈。
有的数据区是多个线程共享的,包括堆、堆外内存(永久代或元空间、代码缓存)
对于方法区和堆,是一个进程对应着一份;而程序计数器本地方法栈、虚拟机栈是一个线程对应着一份。
举个例子:一个程序在运行当中有五个线程,那么就有五组程序计数器、本地方法栈和虚拟机栈,而这五组线程又共有一个方法区一个堆空间
线程
线程是一个程序里的运行单元。JVM允许一个应用有多个线程并行执行。
在Hotspot JVM里,每个线程都与操作系统的本地线程直接映射。
当一个Java线程准备好执行以后,此时一个操作系统的本地线程也同时创建。java线程执行终止后,本地线程也会回收
操作系统负责所有线程的安排调度到任何一个可用的cpu上。一旦本地线程初始化成功。他就会调用java线程中的run方法。
当执行run()方法时若出现了一些捕获的异常,那么java线程就会终止,本地线程要去决定JVM要不要终止。JVM要不要终止还要取决于当前线程是不是最后一个非守护线程(线程分为守护线程、普通线程)。如果只剩守护线程,那么JVM就可以被终止了。
程序计数器(PC寄存器)
JVM中的程序计数寄存器(Program Counter Register),Register的命名源于cpu的寄存器,存储器存储指令相关的现场信息,cpu只有把数据装载到寄存器中才能运行
JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。
作用
PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
它是一块很小 的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
在JVM规范中,每个线程都有它自己的程序计数器,是线程私有的,声明周期与线程 的生命周期保持一致
任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前正在执行的java方法的jvm指令地址;或者如果是在执行native方法,则是未指定值。
它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计算器来完成。
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
它是唯一一个在java虚拟机规范中没有规定任何OutOtMemoryError情况的区域
关于PC寄存器的使用举例
写了一段简单的java代码
public class PCRegister {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
}
}
反编译后的main方法
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
最左边的这一竖排数字叫指令地址,也叫偏移地址,也就是PC寄存器存储的数据,每次只存储一个指令地址。然后执行引擎会去取这个指令地址所代表的指令,然后执行引擎去操作局部变量表、操作数栈,实现数据的存、取等操作。当然执行引擎也会把字节码指令翻译成机器指令。然后机器指令让cpu去做运算。
常见问题
1.使用PC寄存器存储字节码指令地址有什么用呢?为什么使用PC寄存器记录当前线程的执行地址呢?
因为CPU需要不停的切换各个线程,这时候切换回来以后,就知道接着从哪开始继续执行。
JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。
2 PC寄存器为什么会被设定为线程私有
所谓的多线程在一个特定的时间段内只会执行其中一个线程的方法,CPU会不停的做任务切换,这样必然导致经常中断或恢复。为了能够准确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间可以进行独立计算,不会出现相互干扰的情况。
由于CPU时间片轮转限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器的一个内核,只会执行某个线程中的一条指令。这样必然会导致经常中断或恢复。在每个线程创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。