引言
本博客总结自《深入理解 Java 虚拟机》,第二章。
一、概述
Java 虚拟机在执行 Java 程序的时候会把它所管理的内存划分为若干个不同的数据区域。
记忆口诀:两栈一计数,一堆一方法。
解释:第一句两栈分别是VM栈和本地方法栈,一计数指的是程序计数器,它们都是线程私有;后一句,一堆指的是Java堆,一方法指的是方法区,这两个区域是线程共享。
二、程序计数器
程序计数器是一个较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
在虚拟机的概念模型(实现各有不同)中字节码解释器工作时就是通过改变程序计数器的值来选取下一条需要执行的字节码指令,这与程序的分支、循环、跳转、异常处理、线程恢复等基础功能紧密相关。
在这里,书中提到:
Java 虚拟机的多线程是通过线程轮流切换并分配一个 CPU 内核执行时间的方式来实现的,任何一个时间点,一个CPU 内核都只会执行一条线程中的指令。
正因为这样的线程实现策略,要想在线程切换后恢复到正确的执行位置,这个程序计数器必须是线程私有,独立存储。
程序计数器可以存储两种内容,字节码指令地址或空值。
当线程正在执行的是 Java 方法的时候,存储字节码指令地址,而当执行的是Native 方法的时候,则为空值。
另外,值得注意的是,此内存区域是 JVM 规范中唯一没有规定任何 OOM Error情况的区域。也就是说,如果你的程序突然报了 OutOfMemory Error 错误,那么肯定不是这个区域出了问题。
三、JVM 栈
JVM 栈又称为 Java 虚拟机栈,或 VM 栈。
这块逻辑分区存储的方法调用信息。Java 方法在调用的时候,会在 JVM 栈中创建一个 栈帧(Stack Frame),它会用于存储方法内部所用到的局部变量表、操作数栈、动态链接、方法返回地址等信息。其中最需要程序开发人员关心的是存储局部变量的局部变量表。
方法开始执行,栈帧入栈,方法执行完毕,栈帧出栈,以此来实现线程中方法的调用。
因此,从上面的特征来看, JVM 栈也一定是线程私有的。生命周期与线程相同。
局部变量表,存储编译期可知的基本数据类型变量(double、long 占 2 个 Slot 局部变量空间,其他 1 个)、对象引用、以及 returnType。
局部变量表的内存分配是在编译期间完成分配。当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
一般情况下 JVM 栈不会轻易抛出异常,但是有一种情况经常会导致 StackOverflowError 异常,就是递归。
递归是一种通过调用方法自身向“基准情况”不断推进的一种算法,但是基准情况的判断无法在编译期给出答案,这就导致了,如果无法在有限的栈深度给出问题的解,方法就会不停地调用自身,从而将 JVM 栈空间占满,那么就会导致 StackOverflowError 异常。但现代的大多数虚拟机都支持动态扩展这块内存区域,但如果连动态申请的内存依然无法满足计算需要,就会报 OutOfMemoryError 异常。