深入理解JVM—程序计数器

1、JVM内存模型概述

Java虚拟机(JVM)在Java程序运行的过程中,会将它所管理的内存划分为若干个不同的数据区域,这些区域有的随着JVM的启动而创建,有的随着用户线程的启动和结束而建立和销毁。一个基本的JVM运行时内存模型如下所示:


图 1 JVM运行时数据区

上图是展示JDK8及以后的虚拟机规范对JVM运行时内存的划分。在JVM的运行时数据区中Java虚拟机桟、本地方法桟和程序计数器是每个线程私有的,同时堆区和方法区(即图中展示的元数据区和代码缓存)是线程共享的内存。也就是说堆和方法区在一个JVM中各自只有一份,它们是随着JVM的启动而创建的,但是Java虚拟机桟、本地方法桟和程序计数器是线程私有的,每个线程一份,多个线程就可以随线程的启动创建多份,它们的关系可以用下面的图来表示:


图 2 JVM运行时数据区

2、程序计数器(PC)

2.1 什么是程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里(仅是概念模型,各种虚拟机可能会通过一些更高效的方式去实现),字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
                        —— 摘自《深入理解JAVA虚拟机》

2.2 程序计数器的特点

1)线程私有,随着线程的启动而创建,并且随着线程的销毁而销毁(即生命周期和线程的生命周期一样)。
2)执行Java方法时,程序计数器是有值的,且记录的是正在执行的字节码指令的地址;当执行Native方法的时候。计数器的值为空(Undefined)。
3)程序计数器内存区域是Java虚拟机规范中唯一没有规定OOM(OutOfMemoryError)的区域。

2.3 关于程序计数器的几个常见问题

(1)使用PC存储字节码指令地址有什么用呢/为什么要使用PC来记录当前线程的执行地址呢?
因为Java 虚拟机的多线程是通过切换线程并分配处理器执行时间的方式来实现的,在任何一个确定的时间,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令,因此当CPU不停的切换各个线程,下次切换回来后就需要知道接着从哪个地方开始继续执行。使用PC就可以记录下切换前的下一条字节码指令的地址,下次切换回来后直接跳转到PC所记录的地址接着执行即可。

(2)PC为什么要设定成为线程私有的?
在Java的多线程环境下,多个线程如果像共享堆内存一样共享一个PC寄存器,那么前一个线程刚保存的下一条字节码指令地址数据就会被下一个线程覆盖,最终还是和没有使用PC是一样的效果。因此为了能够精确的记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程分配一个PC寄存器,毕竟PC的体积也很小。

(3)为什么执行Native方法的时候PC的值是Undefined?
因为native方法是Java通过JNI直接调用本地C/C++库(可以近似的认为native方法相当于C/C++暴露给Java的一个接口,Java通过调用这个接口从而调用到C/C++方法),由于该方法是通过C/C++而不是Java进行实现。那么自然无法产生相应的字节码,并且C/C++执行时的内存分配是由C/C++语言决定的,而不是由JVM决定的。

(4)为什么PC不会产生OOM?
程序计数器保存的是当前执行的字节码的偏移地址(也就是之前说的行号,其实那不是行号,是指令的偏移地址,只是为了好理解,才说是行号的),当执行到下一条指令的时候,改变的只是程序计数器中保存的地址,并不需要申请新的内存来保存新的指令地址;因此,永远都不可能内存溢出的。

留言区

还能输入500个字符