Back
Featured image of post JVM 学习笔记 02 - JVM 宏观内存分区

JVM 学习笔记 02 - JVM 宏观内存分区

JVM 学习笔记,本文讲述宏观上 JVM 定义的几个内存区块。

Intro

上一篇大致了解了 JVM 是如何实现跨平台的,本节介绍一下 JVM 的宏观内存分区。

如果想要在 native 语言中创建数据并在运行时动态分配,以 C 语言为例,可能要这么做:

#include <stdlib.h>
#include <stdio.h>

int main() {
  int *mem = malloc(sizeof(int) * 4);
  mem[0] = 10;
  mem[1] = 2;
  for (int =0; i < 4; i++) {
    printf("%d, ", mem[i]);
  }
  free(mem);
  mem = NULL;
}

手动分配内存需要很多开发精力,一次创建需要严格对应一次释放,没有释放会造成内存泄漏,重复释放更会导致不可预测的运行时错误,甚至是让程序崩溃。

Java 不需要也不允许开发者手动管理内存,通过垃圾回收器(GC,Garbarge Collect)机制,Java 可以帮程序员管理内存,从而减少出错的可能。

虽然这带来了极大的便利,除了带来性能上的损失,一旦出现内存问题,如 OutOfMemoryError(OOM) 等。开发者同样无法直接修改底层代码,精细地控制内存。所以才需要了解 JVM 内存管理机制,以便在出现此类问题时,找到解决之道。

JVM 内存布局

下图展示了 JVM 的运行时内存布局:

JVM 数据区

程序计数器(Program Counter Register)可以看为 thread local 字节码的行号指示器。控制流基于 goto,而 goto 所需要的行号就是这块内存提供的。

Thread Local(本地线程)就是当前正在执行的线程,一个核心在同一时刻只能执行一条指令。每个计数器都对应一个 CPU 核心,互不干扰,因此称作 Thread Local。


虚拟机栈(VM Stack)同样也是 thread local 的。这和 C 等语言中的概念类似,不过是抽象出的一层虚拟栈。存放各种基本数据类型、对象引用、字节码地址。

每个方法在执行时都会创建对应的栈帧(Stack Frame),存储局部变量表、操作数栈、动态链接、方法出口等信息。局部变量表的大小在编译时是可知的。然而此处的大小也是虚拟的概念,是指 Slot 的数量。具体大小是由 JVM 实现自行决定的。一般来说会用 32bit 作为一个变量槽,所以 64 位 long 和 double 会占用 2 slots。

在栈中的内存错误可能有两种情况:

  1. 栈深度超长,例如无限递归调用时,会抛出 StackOverflowError
    • Android 中的栈大小更被限制,因此常常通过 inline 的方法减少调用栈,在 Compose 的源码中更是随处可见 inline 修饰符(当然,这是 Kotlin 的特性)
  2. 若 JVM 实现允许动态扩容,却无法申请足够内存时,会抛出 OutOfMemoryError

在最广泛使用的 Hotspot VM 实现中,栈不允许动态拓展,所以多数情况下,只有第一种情况出现。


本地方法栈(Native Method Stacks)和 VM Stack 类似。不过这是为 native 方法准备的。实质上 JVM 标准也未定义统一的规范,虚拟机可以根据需要自行实现。


Heap 堆内存是 JVM 中最大的一块内存,被所有线程共享。Java 对象实例大多都分配在这里。是 GC 主要管理的内存区域,所以也被称为 GC 堆。

Heap 可能是固定大小的,也可能是可扩容的,这取决于虚拟机实现。不过大多数 JVM 都允许通过 -Xmx -Xms 参数指定。如果 Heap 空间耗尽,且无法再扩容,此时也会抛 OOM。

再说一句,JVM 规范中没有对 Heap 做细分,「新生代」「老年代」「永久代」「Eden」「Survivor」等概念都是只特定于「分代 GC」的术语。如今 Java 中 G1、ZGC 等分区 GC 出现,就更不应该将这些术语认为是标准了。


方法区(Method Area)也是共享的,用于存储运行时可获取的类型信息、常量、静态变量、JIT 编译后的缓存等数据。也叫做 Non-heap。

HotSpot 以前有一个永久代(Permanent Generation)的概念,现在已经完全废除了。虽然 JVM 规范中对此区域的限制极少,甚至可以不实现 GC。因为该区域确实也不怎么需要 GC,主要是常量池的回收。

运行时常量池(Runtime Constant Pool)是方法区的一部分,.class 文件中除了版本、字段、方法、接口等描述信息外,还有一项就是常量表了。用于生成编译时的字面量和符号引用。在运行时就会被加载入此处。

同时,通过反射等方式,也能够动态在此处添加常量……所以有动态类生成之类的技术存在。

方法区申请不到内存时,同样也会爆 OOM。


此外,在 JVM 定义外,还存在直接内存(Direct Memory),也就是堆外内存。这块内存不受 JVM 管理,例如在 NIO 中引入了 Channel 和 Buffer 数据的 IO 方式,可以直接使用 Native 堆,而不必反复在 JVM 和 Native 间复制,显著提高了性能。

这块内存不受 -Xms 限制,但受实际操作系统和物理内存限制,容易被忽略。导致内存区域总和大于物理限制,从而出现 OOM。

介绍完 JVM 总体的内存分配,下一篇文章将会讲解对象创建的具体细节。

comments powered by Disqus