之前有很多人将 Java 的内存分为堆内存(heap)和栈内存(Stack),这种划分方式在一定程度上体现了这两块区域是 Java 工程师最关注的内存区域。但是其实这种划分方式并不完全准确。
Java 的内存区域划分实际上远比这复杂:Java 虚拟机在执行 Java 程序的过程中,会把它所管理的内存划分为不同的数据区域。下面这张图描述了一个 HelloWorld.java 文件被 JVM 加载到内存中的过程:
HelloWorld.java 文件首先需要经过编译器编译,生成 HelloWorld.class 字节码文件。
Java 程序中访问HelloWorld这个类时,需要通过 ClassLoader(类加载器)将HelloWorld.class 加载到 JVM 的内存中。
JVM 中的内存可以划分为若干个不同的数据区域,主要分为:程序计数器、虚拟机栈、本地方法栈、堆、方法区。
1.1 程序计数器(Program Counter Register)
Java 程序是多线程的,CPU 可以在多个线程中分配执行时间片段。当某一个线程被 CPU 挂起时,需要记录代码已经执行到的位置,方便 CPU 重新执行此线程时,知道从哪行指令开始执行。这就是程序计数器的作用。
“程序计数器”是虚拟机中一块较小的内存空间,主要用于记录当前线程执行的位置。
如下图所示:每个线程都会记录一个当前方法执行到的位置,当 CPU 切换回某一个线程上时,则根据程序计数器记录的数字,继续向下执行指令。
实际上除了上图演示的恢复线程操作之外,其它一些我们熟悉的分支操作、循环操作、跳转、异常处理等也都需要依赖这个计数器来完成。
关于程序计数器还有几点需要格外注意:
在 Java 虚拟机规范中,对程序计数器这一区域没有规定任何 OutOfMemoryError 情况(或许是感觉没有必要吧)。
线程私有的,每条线程内部都有一个私有程序计数器。它的生命周期随着线程的创建而创建,随着线程的结束而死亡。
当一个线程正在执行一个 Java 方法的时候,这个计数器记录的是正在执行的虚拟机字节码指令的地址。如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。
1.2 虚拟机栈
虚拟机栈也是线程私有的,与线程的生命周期同步。在 Java 虚拟机规范中,对这个区域规定了两种异常状况:
StackOverflowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出。
OutOfMemoryError:当 Java 虚拟机动态扩展到无法申请足够内存时抛出。
在我们学习 Java 虚拟机的的过程当中,经常会看到一句话:
JVM 是基于栈的解释器执行的,DVM 是基于寄存器解释器执行的。
上面这句话里的“基于栈”指的就是虚拟机栈。虚拟机栈的初衷是用来描述 Java 方法执行的内存模型,每个方法被执行的时候,JVM 都会在虚拟机栈中创建一个栈帧,接下来看下这个栈帧是什么。
栈帧
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,每一个线程在执行某个方法时,都会为这个方法创建一个栈帧。
我们可以这样理解: