Intro
上一篇大致了解了 JVM 是如何实现跨平台的,本节介绍一下 JVM 的宏观内存分区。
如果想要在 native 语言中创建数据并在运行时动态分配,以 C 语言为例,可能要这么做:
#include <stdlib.h>
#include <stdio.h>
int main() {
int *mem = malloc(sizeof(int) * 4);
[0] = 10;
mem[1] = 2;
memfor (int =0; i < 4; i++) {
("%d, ", mem[i]);
printf}
(mem);
free= NULL;
mem }
手动分配内存需要很多开发精力,一次创建需要严格对应一次释放,没有释放会造成内存泄漏,重复释放更会导致不可预测的运行时错误,甚至是让程序崩溃。
Java 不需要也不允许开发者手动管理内存,通过垃圾回收器(GC,Garbarge Collect)机制,Java 可以帮程序员管理内存,从而减少出错的可能。
虽然这带来了极大的便利,除了带来性能上的损失,一旦出现内存问题,如
OutOfMemoryError
(OOM)
等。开发者同样无法直接修改底层代码,精细地控制内存。所以才需要了解 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。
在栈中的内存错误可能有两种情况:
- 栈深度超长,例如无限递归调用时,会抛出
StackOverflowError
- Android 中的栈大小更被限制,因此常常通过 inline 的方法减少调用栈,在 Compose 的源码中更是随处可见 inline 修饰符(当然,这是 Kotlin 的特性)
- 若 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 总体的内存分配,下一篇文章将会讲解对象创建的具体细节。