深蓝黑背景封面:左侧是 JVM 运行时数据区地图——一个大圆角框标注「JVM 运行时数据区」,框内左半紫色「线程私有」竖列三张卡:程序计数器(规范中唯一不会溢出的区域)、虚拟机栈(StackOverflowError / OOM)、本地方法栈(SOE / OOM);右半青色「线程共享」竖列两张大卡:堆(OOM:heap space)、方法区(OOM:Metaspace);框外下方灰色虚线卡片「直接内存(堆外)」标注 OOM:Direct buffer memory,并用虚线与堆相连表示它不占堆。右侧大标题「JVM 内存区域全景图」,副标题「六个区域,各自的溢出形态」;左上标注「知识延伸 · JVM 内存」,右上圆角胶囊标注「Day 08 · 11-15」 图 1:本文封面,JVM 运行时数据区的一整张地图。

同批的面试真题解析讲了虚拟机栈这一个区域的两种溢出:容量到顶抛 StackOverflowError,扩容失败抛 OutOfMemoryError。这篇把镜头拉远:JVM 运行时到底有哪几块内存区域,每块存什么、怎么溢出、出问题从哪儿入手排查。记住这张全景图,绝大多数「内存爆了」的报警,你都能先定位到区域,再谈工具。

先分两堆:线程私有 vs 线程共享。

JVM 规范第 2.5 节把运行时数据区按生命周期分成两类。线程私有区域随线程创建而创建、随线程销毁而销毁:程序计数器、虚拟机栈、本地方法栈,每多一条线程就多一份,彼此隔离。线程共享区域随虚拟机启动而创建、随虚拟机退出而销毁:堆和方法区,整个进程只有一份,所有线程一起用。在这两堆之外,还有一块规范没直接管辖、但工程里天天打交道的直接内存(堆外内存)。先立起这个坐标系,后面每个区域都只是往格子里填内容。

程序计数器:唯一一个规范保证不溢出的区域。

每条线程的程序计数器记录当前正在执行的字节码指令地址,相当于「执行到第几行」的书签。它需要的空间是固定且可预测的,所以规范没有为它规定任何异常条件——这是整个数据区里唯一永远不会溢出的角落。排查内存问题永远不用看它,但要知道它存在:线程转储里每个线程的「当前执行位置」,本质就是它。

虚拟机栈与本地方法栈:一对兄弟区域。

虚拟机栈每调用一个方法压入一帧,帧里装着局部变量表和操作数栈;本地方法栈则为 native 方法的调用服务。规范给它们列了同款两条异常路径:深度超限抛 StackOverflowError,动态扩展或创建线程时申请不到内存抛 OutOfMemoryError。栈帧结构与 -Xss 的跷跷板效应,同批真题解析已逐层展开,这里只记结论:线程转储里看到几千层重复调用,先怀疑栈;-Xss 调多大,决定的是单线程深度与可建线程数如何此消彼长。

堆:对象的家,OOM 的主战场。

所有对象实例和数组都在堆上分配,垃圾回收器在这里工作。规范的口径很直白:自动存储管理系统给不出更多堆内存时,抛 OutOfMemoryError。HotSpot 里它的消息通常是 Java heap space,容量闸门是 -Xmx 与 -Xms。堆内部还分新生代、老年代,那是另一个话题;在区域地图上只需记住:看到 heap space,先 dump 堆、分析对象分布。

方法区:装「类」的地方,HotSpot 里叫元空间。

方法区存的是类型信息、运行时常量池、静态变量这些类级别的数据。规范同样给它规定了溢出形态:方法区内存满足不了分配需求时,抛 OutOfMemoryError。HotSpot 用元空间(Metaspace)实现它,闸门是 -XX:MaxMetaspaceSize,溢出消息是 Metaspace。典型触发场景是类加载失控:动态生成类的框架、频繁热部署的应用、类加载器泄漏,都会把这个区域喂爆。

直接内存:堆外的那一块,消息里写得清清楚楚。

NIO 的 ByteBuffer.allocateDirect 分配的缓冲区,按 JDK 官方文档的说法,其内容可以驻留在正常垃圾回收堆之外——好处是底层 IO 操作少一次拷贝,代价是它不受 -Xmx 管辖,占的是进程的真实内存。它有独立的闸门 -XX:MaxDirectMemorySize,超限时同样抛 OutOfMemoryError,消息是 Direct buffer memory。堆看着很空闲却报 OOM,多半要往这个方向想。

深蓝黑背景,主体是一张六行五列的对照矩阵,列为「区域、线程归属、存什么、溢出形态、排查入口」。六行依次:程序计数器(线程私有,存当前线程字节码行号,规范中唯一不会溢出的区域,无需排查);虚拟机栈(线程私有,存栈帧:局部变量表与操作数栈,溢出为 StackOverflowError 深度超限或 OOM 栈动态扩展失败,排查看线程转储调用深度并调整 -Xss);本地方法栈(线程私有,存 native 方法调用帧,SOE / OOM,排查线程转储中的 native 帧);堆(线程共享,存对象实例与数组,OOM:Java heap space,排查用 -Xmx/-Xms 与堆转储分析对象分布);方法区(线程共享,存类型信息、运行时常量与静态变量,OOM:Metaspace,排查 MaxMetaspaceSize 与类加载);直接内存(堆外,存 NIO DirectBuffer,OOM:Direct buffer memory,排查 MaxDirectMemorySize)。区域名前用紫、青、灰三色圆点区分线程私有、线程共享与堆外。底部总结条:排查第一步看异常类型——SOE 找栈,OOM 再看消息里的区域名 图 2:区域 × 溢出形态 × 排查入口的对照矩阵。

排查动线:异常类型定区域,区域定工具。

把上面六格串成一条动线:报警来了先看异常类型——StackOverflowError 直接指向栈,拉线程转储看调用深度;OutOfMemoryError 则先读消息里的区域名:heap space 就对照 -Xmx 做堆转储、分析对象分布,Metaspace 就查 MaxMetaspaceSize 与已加载类的数量,Direct buffer memory 就查 MaxDirectMemorySize 与 NIO 缓冲用量。一句话口诀:SOE 找栈,OOM 先看消息再动手。区域名是 JVM 亲手写给你的排查入口,别跳过它直接去翻堆 dump。

这张地图里,你日常打交道最多的是哪一格?有没有遇到过「堆明明很空却报 OOM」的情况,最后发现是哪个区域在作怪?欢迎在评论区对号入座。