图 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,多半要往这个方向想。
图 2:区域 × 溢出形态 × 排查入口的对照矩阵。
排查动线:异常类型定区域,区域定工具。
把上面六格串成一条动线:报警来了先看异常类型——StackOverflowError 直接指向栈,拉线程转储看调用深度;OutOfMemoryError 则先读消息里的区域名:heap space 就对照 -Xmx 做堆转储、分析对象分布,Metaspace 就查 MaxMetaspaceSize 与已加载类的数量,Direct buffer memory 就查 MaxDirectMemorySize 与 NIO 缓冲用量。一句话口诀:SOE 找栈,OOM 先看消息再动手。区域名是 JVM 亲手写给你的排查入口,别跳过它直接去翻堆 dump。
这张地图里,你日常打交道最多的是哪一格?有没有遇到过「堆明明很空却报 OOM」的情况,最后发现是哪个区域在作怪?欢迎在评论区对号入座。