【JVM第三篇--运行时数据区】程序计数器、虚拟机栈、本地方法栈

时间:2023-03-09 22:43:51
【JVM第三篇--运行时数据区】程序计数器、虚拟机栈、本地方法栈

写在前面的话:本文是在观看尚硅谷JVM教程后,整理的学习笔记。其观看地址如下:尚硅谷2020最新版宋红康JVM教程

一、运行时数据区

我们在编写Java程序时,使用JVM的流程主要如下所示:

【JVM第三篇--运行时数据区】程序计数器、虚拟机栈、本地方法栈

虚拟机在执行Java程序时,会把它所管理的内存划分为不同的数据区域,即运行时数据区。有些数据区域是线程共享的,即这些区域会随着虚拟机的启动而创建,随着虚拟机的关闭而销毁。而另一些区域则是与线程对应,属于线程私有的。这些区域会随着线程开始而创建,随着线程的结束而销毁。

具体的划分如下:

多个线程共享的:堆、方法区

每个线程私有的:程序计数器、Java虚拟机栈、本地方法栈

图示如下:

【JVM第三篇--运行时数据区】程序计数器、虚拟机栈、本地方法栈

关于Java线程:在虚拟机中,每个线程都与操作系统的本地线程是直接对应的。当Java的线程准备执行时,操作系统的线程也同时创建了。

二、程序计数器

程序计数器是一块很小的内存空间,可以认为是当前线程所执行的字节码的行号指示器,负责保存当前线程正在执行的字节码地址。

1、程序计数器的作用

① Java是支持多线程的,这也意味着CPU会不停地切换线程。虚拟机的多线程是通过CPU时间片轮转法来实现的,即每个线程占用CPU的时间是同等的,时间一到就会切换线程,如此重复,直到线程终止。这也意味着某个线程会因为时间片到而被挂起,所以当该线程再次获得时间片时,需要知道从哪个地方继续执行,此时虚拟机只需要读取程序计数器的值就可以知道要执行的字节码指令的位置了。这也是程序计数器是线程私有的原因,因为若程序计数器不是线程私有的话,当CPU切换线程时,会按照上一个线程的字节码指令的位置来执行当前线程的字节码指令,这显然是不正确的

② JVM的字节码解释器也需要通过改变程序计数器的值来明确下一条字节码指令。

2、程序计数器的特点

① 程序计数器是数据区中唯一没有规定OutOfMemoryError(内存溢出异常)的区域。

②如果一个线程执行的是Native本地方法,那么程序计数器的值为undefined。因为JVM在执行Native本地方法时,是通过JNI调用本地其他语言来实现的,而不是字节码。

三、虚拟机栈

栈是运行时的单位,堆是存储的单位。

栈解决程序的运行问题,即方法如何执行(或者如何处理数据)。

堆解决的是数据存储问题,即数据怎么放,在那放。

1、什么是虚拟机栈

虚拟机栈在每个线程开始时随之创建,虚拟机栈与数据结构中的栈一致,也是遵循先进后出的原则。

虚拟机栈进行出栈入栈操作的元素就是栈帧每一个栈帧就对应着一个方法,也可以将栈帧理解为一个方法的运行空间

一个栈帧入栈,意味着一个方法被调用,一个栈帧出栈,即一个方法执行完毕。则栈帧的入栈顺序就是方法的调用顺序。

有代码:

public class VMStackTest {

    private static int i = 1;

    public static void main(String[] args) {
VMStackTest test = new VMStackTest();
test.add();
System.out.println(i);
} public void add(){
i++;
}
}

代码所示的虚拟机栈示例如下:

【JVM第三篇--运行时数据区】程序计数器、虚拟机栈、本地方法栈

同一时刻,在同一线程中,只有位于虚拟机栈顶部的栈帧才是有效运行的,即只有位于栈顶的方法是正在执行的。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。 如果在被调用的A方法中,又调用了B方法,那么对应的B方法的栈帧就会被创建,并被放在虚拟机栈的栈顶,成为新的正在执行的方法。

对于虚拟机栈来说,不存在垃圾回收问题。

Java方法有两种方法返回方式,一种是正常的方法返回,使用return指令;另一种是抛出异常。不管是哪一种,都会导致栈帧被弹出。

2、栈帧的内部结构

每个栈帧都存储着:

① 局部变量表

②操作数栈

③方法返回地址

④动态链接

⑤一些附加信息

图示如下:

【JVM第三篇--运行时数据区】程序计数器、虚拟机栈、本地方法栈

(1)、局部变量表

局部变量表定义为一个数字数组,用于存放方法参数和方法内定义的局部变量

局部变量表建立在线程的虚拟机栈上,是线程的私有数据,因此不存在数据安全问题。

虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。

局部变量表所需的容量大小是在编译期定下的,方法运行期间是不会该变局部变量表的大小的。

(2)、操作数栈

操作数栈也是一个栈的数据结构,操作数栈的最大深度也在编译的时候定下。

操作数栈的每一个元素可以是任意Java数据类型,在方法执行过程中,操作数栈的深度都不会超过最大值。

操作数栈的作用是: 随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈,再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者,也就是出栈/入栈操作。一个完整的方法执行期间往往包含多个这样出栈/入栈的过程。

(3)、方法返回地址

一个方法结束,有正常执行完毕退出和抛出异常(没catch),非正常退出两种。

无论是那种方式,在退出后都会返回到该方法被调用的位置。

正常退出时,调用者的程序计数器的值会作为返回地址,即调用该方法的指令的下一条指令的地址。但是异常退出时,返回地址要通过异常表来确定。

实际上,方法的退出就是当前栈帧出栈的过程。故需要恢复上一层栈帧的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈,并设置其程序计数器的值等,让调用者栈帧继续执行下去。

两种方法结束的区别是:因抛出异常而退出的方法不会给调用他的调用者栈帧任何返回值

3、虚拟机栈的常见异常

《Java虚拟机规范》中,允许虚拟机栈的大小是动态扩展的或者固定不变的。

这意味着,虚拟机栈会出现两种异常:

*Error异常(栈溢出):

如果线程请求分配的栈容量超过Java虚拟机的最大容量时,就会抛出栈溢出异常。最简单的示例就是不停递归而不退出,就会报 *Error

当虚拟机栈的大小为固定时,易出现该异常。

OutOfMemoryError异常(内存溢出)

当虚拟机栈采用动态扩展时,一定程度上可以规避 *Error,但是虚拟机给每个线程分配到的内存空间是有限的,这也意味着隶属于线程的虚拟机栈的内存也是有限的,所以当虚拟机栈请求扩展的内存大小无法满足时,就会报该异常。

但是并不意味着,采用固定大小的虚拟机栈就不会报该异常,如果某个栈帧太大,超出虚拟机栈所有的内存也是可能的。

四、本地方法栈

(1)、本地方法

一个Native Method就是一个Java调用非Java代码的接口。在定义一个本地方法时,并不提供实现体(有些就像定义一个Java interface),因为实现体是由非Java语言在外面实现的。本地接口的作用是融合不同的编程语言为Java所用。

(2)、本地方法栈

虚拟机栈是用于管理Java方法的调用,本地方法栈是用于管理本地方法的调用。

①本地方法栈是线程私有的

②本地方法栈也是可实现固定或者可动态扩展的内存大小(这也意味着本地方法栈的异常也与虚拟机栈一致)

③本地方法栈存放着被native关键字标记的方法,在执行引擎执行时加载本地方法库。

④并不是所有的JVM都支持本地方法,故也无需实现本地方法栈

⑤在HotSpot JVM中,直接将本地方法和虚拟机栈合二为一。

五、扩展--设置栈内存大小

有代码如下:

public class StackOverError {
private static int count = 1;
public static void main(String[] args) {
System.out.println(count);
count++;
main(args);
}
}

在代码中采用了递归,以count++的次数来查看目前虚拟机栈的深度。其结果如下:

【JVM第三篇--运行时数据区】程序计数器、虚拟机栈、本地方法栈

可见默认情况下,count的值为2467。

我们可以使用

-Xss:来规定了每个线程虚拟机栈的内存大小

如下,

【JVM第三篇--运行时数据区】程序计数器、虚拟机栈、本地方法栈

设置栈的大小:--Xss1m或者-Xss1k (设置方法IDEA:Run --> Edit Configuration --> VM option)

结果如下,

【JVM第三篇--运行时数据区】程序计数器、虚拟机栈、本地方法栈

可见当设置虚拟机栈的内存大小为2m时,count的值为233424。

故可看出使用参数 -Xss可以设置线程的最大栈空间,且虚拟机栈的大小直接决定了函数调用的大小。

参考:

知乎-JVM虚拟机栈执行原理深入详解

2020最新-JVM-Java虚拟机-从入门到精通-尚硅谷

知乎-Java虚拟机—栈帧、操作数栈和局部变量表