JVM 运行时的数据区域

JVM 在运行时,会按照程序执行的需要来创建一系列的运行时数据区域。有的区域只会随 JVM 起停而被创建和销毁,有的区域则会独立分配给各个线程,并随线程的起停而创建和销毁。这些运行时区域,按照功能和性质不同,会分成如下几部分:

pc (program counter) 寄存器

JVM 允许同时运行多个线程,每个线程都有它自己的 PC 寄存器。在任意时刻,每个 JVM 线程都在执行一个方法中的某条语句,而这个正在被执行的方法,就叫做这个线程的 “当前方法”。

如果当前方法不是一个本地 (native) 方法,那么 PC 寄存器的内容是当前正在执行的指令的地址;如果当前方法是本地方法,那么 PC 寄存器的值则是空 (undefined) 的。

JVM 栈

每个 JVM 都会在其启动时创建自己私有的 JVM 栈,栈之中存储的是栈帧,用于存储局部变量和方法调用信息。

规范中允许栈的深度可以是固定的,也可以根据要求动态的扩展和收缩。如果是固定深度的栈,那么每个栈的深度会在其创建时按照需要独立指定。

当请求创建的栈大于所允许的深度,那么 JVM 会抛出 StackOverflowError 异常;当程序试图扩大一个可以动态伸缩的栈,或者试图为新的线程创建一个栈,但是可用内存不足以完成这个操作时,那么 JVM 会抛出 OutOfMemoryError 异常。

本地方法栈

本地方法栈与 JVM 栈类似,保存了本地方法的调用信息。

本地方法栈的空间可以是固定的,也可以是动态伸缩的。

当程序申请了大于所允许的本地方法栈空间,那么 JVM 会抛出 StackOverflowError 异常;如果程序申请扩展一个可以动态伸缩的本地方法栈,或者试图创建一个栈,但是可用内存不足以满足要求时,JVM 会抛出 OutOfMemoryError 异常。

在 JVM 启动时,会创建一个共享于所有线程的堆空间,其中存放着所有的对象,和被分配好空间的数组。用于存放对象的空间由一个自动化的存储空间管理机制,即垃圾回收机制 (garbage collector),来进行管理。堆空间可以是固定大小的,也可以是按需伸缩的。

如果程序试图申请扩大堆空间,但是存储管理机制无法满足需求时,JVM 会抛出 OutOfMemory 异常。

在堆中,JVM 又根据作用不同,将内存空间分为如下几部分:

新生代 (New generation)

新生代保留的是生命周期短,并且很快就会被回收掉的对象。其中的空间又随着 “复制算法” 这一垃圾回收算法而被分为 Eden SpaceSurvivor Space。具体可以参考 Java 的垃圾回收算法这篇博文。

老年代 (Tenured generation)

在多次垃圾回收后仍然存活的对象,将会被放到老年代空间中。因此可以认为,老年代中的对象的生命周期都是比较长的。

方法区

方法区 (method area) 是一个共享于所有 JVM 线程的空间,创建于 JVM 启动时,其中主要存放的是类的元数据,包括类的类型信息、常量池、方法数据、方法的代码等,这些数据主要来源于 class 文件。方法区逻辑上属于堆的一部分,但是为了与堆区分开来,方法区通常又叫非堆

类型信息包括类的完整名称、父类的完整名称、类型修饰符 (private/protected/public),和类型的直接接口类表。

方法的数据包括方法的名称、返回类型、参数、方法的修饰符、字节码、操作数栈和方法栈帧的局部变量区大小,和异常表。

方法区的大小可以是固定的,也可以是按需伸缩的,但是根据虚拟机实现的不同,垃圾回收机制可能不会回收或压缩方法区的空间。

如果方法区的可用内存无法满足一次申请空间的请求,那么 JVM 会抛出 OutOfMemoryError 异常。

永久代和 Metaspace

在 HotSpot VM 中,永久代和 Metaspace 就是方法区的具体实现。在 Java 8 之前,方法区是以永久代的形式存在的;而从 Java 8 之后,永久代就被 Metaspace 取而代之了。

在 Java 1.7 和之前版本中,永久代是一块独立于堆的内存空间,在物理内存上与堆是连续的。同时,在 Java 1.7 中,一部分原属于永久代的内容也在逐步被移动到其他位置,比如符号引用被移动到了本地内存 (native memory) 中,字符串常量池和类的静态变量则被移动到了堆中。

从 Java 8 开始,永久代被 Metaspace 取而代之。Metaspace 的内存空间不再与堆连续,而是存在于本地内存中。

运行时常量池

运行时常量池对应 class 文件中的 constant_pool

运行时常量池中包含了数值常量和属性的引用。每个运行时常量池的空间都会在类或接口被创建时生成,并且从方法区中分配空间。在创建运行时方法区时,如果申请的空间大于方法区可提供的空间,那么 JVM 会抛出 OutOfMemoryError 异常。

参考文档

[^1]: 《The Java Virtual Machine Specification (Java SE 8 Edition)》 - 2.5 Run-Time Data Areas
[^2]: 面试官,Java8 JVM 内存结构变了,永久代到元空间
[^3]: 方法区 - JVM 运行时的数据区域
[^4]: 方法区(永久区、元空间) - 深入理解 JAVA 虚拟机(内存模型 + GC 算法 + JVM 调优)
[^5]: Java8 内存模型 — 永久代 (PermGen) 和元空间 (Metaspace)