JVM 运行时内存区域

举报
yd_227606267 发表于 2026/10/29 09:37:53 2026/10/29
【摘要】 拆解 JVM 各内存区域的职责、边界与溢出场景

线上出现 OutOfMemoryError 时,报错信息往往只给出一个区域名,比如 Java heap space 或 Metaspace。如果分不清这些区域由谁分配、由谁回收、受哪个参数控制,排查就只能靠猜。本文按 JVM 规范逐个拆解运行时数据区,并以 JDK 8 与 JDK 17 为基准说明实现差异,适合已经写过 Java、但还没系统梳理过内存模型的开发者。

一张内存地图

JVM 规范定义了 5 个运行时数据区,HotSpot 在此之外还使用直接内存。按线程是否共享,可以分成两类:

| 区域 | 线程归属 | 主要存放内容 | 溢出错误 | 关键参数 |

| --- | --- | --- | --- | --- |

| 程序计数器 | 私有 | 当前字节码行号 | 无 | 无 |

| 虚拟机栈 | 私有 | 栈帧、局部变量表 | StackOverflowError / OutOfMemoryError | -Xss |

| 本地方法栈 | 私有 | native 方法栈帧 | 同上 | -Xss |

| 堆 | 共享 | 对象实例、数组 | Java heap space | -Xms / -Xmx |

| 方法区 | 共享 | 类元信息、运行时常量池 | Metaspace | -XX:MaxMetaspaceSize |

| 直接内存 | 共享 | NIO 堆外缓冲区 | Direct buffer memory | -XX:MaxDirectMemorySize |

线程私有区域

程序计数器

程序计数器(Program Counter Register)记录当前线程正在执行的字节码行号。分支、循环、跳转、异常处理以及线程切换后的恢复,都依赖这个值。它是唯一不会抛出 OutOfMemoryError 的区域,因为它的容量在类加载完成后就固定了。

执行 native 方法时,程序计数器的值是 undefined,此时它不指向任何字节码指令。

虚拟机栈与栈帧

虚拟机栈的生命周期与线程相同。每调用一个方法,就有一个栈帧入栈;方法返回,栈帧出栈。一个栈帧包含四部分:

  • 局部变量表:以 Slot 为单位,每个 Slot 占 32 位,long 和 double 占两个 Slot。实例方法的第 0 个 Slot 固定是 this。
  • 操作数栈:字节码指令的运算工作区,最大深度在编译期确定。
  • 动态链接:指向运行时常量池中该方法的符号引用。
  • 方法返回地址:正常返回或异常退出的下一条指令地址。

-Xss 控制单个线程栈的大小,64 位 Linux 上默认 1 MB,大致能支撑一万层左右的调用。栈溢出有两种表现:调用深度超过限制抛 StackOverflowError;线程创建过多导致无法申请栈内存,抛 OutOfMemoryError: unable to create new native thread。

HotSpot 把本地方法栈与虚拟机栈合并实现,因此 -Xss 同时作用于两者。

线程共享区域

堆

堆是 JVM 中最大的一块内存,也是垃圾回收的主战场。按分代收集理论,堆被划分为新生代和老年代,新生代又分为 Eden 和两块 Survivor,默认比例 8:1:1,新生代与老年代默认比例 1:2(-XX:NewRatio=2)。

堆是所有线程共享的,但对象分配并不需要每次都加锁。HotSpot 为每个线程划分了一块 TLAB(Thread Local Allocation Buffer),线程优先在自己的 TLAB 中做指针碰撞分配,用满后再申请新的缓冲区。这也是 -XX:+UseTLAB 默认开启的原因。

-Xms 和 -Xmx 分别控制初始堆和最大堆。生产环境通常把两者设为相同值,避免堆反复扩容收缩带来的额外 GC 和抖动。

方法区、永久代与元空间

方法区是规范中的逻辑概念,存放已被虚拟机加载的类元信息、运行时常量池、静态变量和即时编译器编译后的代码缓存。不同 JDK 版本的实现差别很大:

  • JDK 7 及以前用永久代(PermGen)实现,位于堆内,受 -XX:MaxPermSize 限制,动态生成类较多时容易抛 PermGen space。
  • JDK 8 移除了永久代,改用元空间(Metaspace),使用本地内存,由 -XX:MetaspaceSize 和 -XX:MaxMetaspaceSize 控制。
  • JDK 8 起,字符串常量池和静态变量已经移到堆中(JDK 7 就开始迁移)。

改成元空间的核心原因是永久代大小难以预估:它既要装类元数据,又要装常量池,还要和堆共享地址空间,调大调小都容易出问题。元空间使用本地内存后,默认不设上限,类元数据可以随本地内存增长,但也带来了新风险——容器里不设 MaxMetaspaceSize,动态代理和字节码增强可能把物理内存吃光。

-XX:MetaspaceSize 并不是初始大小,而是首次触发元空间 GC 的高水位阈值,默认约 21 MB。每次 GC 后如果元空间仍在增长,这个阈值会被动态抬高。

运行时常量池

运行时常量池是 Class 文件常量池的运行时表示,存放字面量和符号引用。String.intern() 的行为值得单独记住:JDK 7 以后,首次出现的字符串会被放入堆中的字符串常量池,而不是复制到永久代。

直接内存

直接内存不属于运行时数据区,但它是 JVM 进程内存的重要组成部分。NIO 的 DirectByteBuffer 通过 Unsafe.allocateMemory 在堆外分配内存,受 -XX:MaxDirectMemorySize 限制,未显式设置时默认等于 -Xmx。

直接内存的回收依赖 Cleaner 和一次 Full GC:当 DirectByteBuffer 对象被回收时,Cleaner 会调用 free 释放堆外内存。因此会出现「堆很健康,但进程 RSS 持续上涨」的现象,而 heap dump 里完全看不到这部分内存。

一次栈溢出的最小复现

public class StackOverflowDemo {
    private static int depth = 0;

    public static void recurse() {
        depth++;
        recurse();
    }

    public static void main(String[] args) {
        try {
            recurse();
        } catch (StackOverflowError e) {
            System.out.println("depth = " + depth);
            throw e;
        }
    }
}

用 java -Xss256k StackOverflowDemo 运行,输出深度会明显小于默认 1 MB 时的结果。这说明 -Xss 调小并不能修复栈溢出,只会让溢出更早发生。

常见坑 / 最佳实践

  1. 只设 -Xmx 不设元空间上限。使用 CGLIB、Groovy、反射代理或热部署的进程,类元数据会持续增长。建议显式设置 -XX:MaxMetaspaceSize=256m 到 512m,让问题在可控范围内暴露。
  2. 容器内存只按堆大小申请。容器 limit 必须覆盖堆、元空间、线程栈(线程数 × -Xss)、直接内存、Code Cache、GC 自身数据结构以及 glibc 的 arena 开销,通常要在 -Xmx 基础上多留 20% 到 30%。
  3. 用调小 -Xss 的方式处理 StackOverflowError。栈溢出几乎都是递归没有收敛或调用链异常深,正确做法是查代码和框架调用栈,而不是压缩栈空间。
  4. 排查堆外内存只看 heap dump。堆外问题要用 -XX:NativeMemoryTracking=summary 配合 jcmd <pid> VM.native_memory summary,或者用 pmap 观察进程内存段。
  5. 把 -Xms 和 -Xmx 设成不同值。堆动态伸缩会在每次扩容时触发 Full GC,延迟毛刺明显,生产环境应保持一致。

小结

  • JVM 运行时数据区分为线程私有的程序计数器、虚拟机栈、本地方法栈,以及线程共享的堆和方法区。
  • 程序计数器是唯一不会 OOM 的区域;栈会抛 StackOverflowError;堆和方法区分别对应 Java heap space 与 Metaspace。
  • JDK 8 用元空间取代永久代,元空间位于本地内存,默认无上限,必须显式限制。
  • 直接内存不在堆内,heap dump 看不到,是容器 OOMKilled 的常见盲区。
  • 理解每个区域由哪个参数控制,是把「内存不够」变成「哪里不够」的第一步。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。