搜索站内内容

← 返回文章列表

JVM 学习 01:运行时内存区域、对象与引用

按 JVM 八股原始问题整理程序计数器、栈、堆、方法区、直接内存、对象创建和 OOM。

JasonBackendJavaJVMMemoryOOMDiagnostics约 3,000 字大约 8 分钟
云雾在层叠的山岭之间流动

这一篇对应原始 JVM 笔记中的“运行时数据区”部分。面试里常把它问成一串名词:程序计数器、虚拟机栈、堆、方法区、元空间、直接内存。更好的回答方式是按线程是否共享、存什么、可能报什么错误来组织。

JVM 内存区域有哪些

从线程维度看,程序计数器、虚拟机栈和本地方法栈是线程私有的;堆、方法区这一规范概念,以及常被单独讨论的直接内存,则涉及进程或多个线程共享的运行资源。JVM 规范给实现留下了空间:以堆为例,它可以是连续也可以不连续,大小可以固定也可以按需扩展,垃圾收集算法也不是规范强制的唯一实现。

因此回答“JVM 内存区域”时要同时区分规范和 HotSpot 实现。方法区是规范中的逻辑区域;永久代、元空间、字符串常量池的位置则属于具体虚拟机和 JDK 版本演进,不能把某个旧版本的实现细节当作永恒事实。

程序计数器是什么

程序计数器是一块很小的线程私有内存,可以理解为当前线程正在执行哪一条字节码指令的位置记录。字节码解释器依赖它来完成顺序执行、分支、循环、异常处理;线程切换回来后,也依赖各自的计数器恢复到正确位置。执行 native 方法时没有明确的 Java 字节码位置可记录。

它的特点是随线程创建、随线程结束而消失,且内存需求非常小。JVM 规范中,程序计数器是唯一明确不会发生 OutOfMemoryError 的运行时数据区。这个结论不意味着整个线程一定不会因为资源不足而失败;线程栈、native 栈和系统线程资源仍然会受限制。

虚拟机栈和本地方法栈是什么

每个 Java 方法调用都会创建一个栈帧,并压入当前线程的虚拟机栈。栈帧通常包括局部变量表、操作数栈、动态链接和方法返回信息。方法结束后栈帧出栈,因此局部变量的生命周期与调用栈紧密相关。递归过深、循环调用或异常调用链不断叠加时,栈深度可能超过上限,出现 StackOverflowError

若某个虚拟机实现允许栈动态扩展,扩展时无法申请到足够内存也可能表现为 OOM。本地方法栈服务于 native 方法调用;不同虚拟机可能将其单独实现,也可能与虚拟机栈合并。排查栈问题时,先用线程栈确认重复调用模式和深度,不要第一反应只调大 -Xss;更大的单线程栈也会减少同一进程可容纳的线程数。

堆是什么

堆是 JVM 中最大、最常被讨论的共享区域,随虚拟机启动创建。对象实例和数组通常在堆上分配,因此它也是垃圾收集器主要管理的区域。常见实现会把堆划分为年轻代和老年代;年轻代又常见 Eden、Survivor 0、Survivor 1 等区域。

一般对象先在 Eden 分配。年轻代回收后仍存活的对象会进入 Survivor 区并积累年龄,达到条件后可能晋升老年代。年龄阈值、Survivor 空间、对象大小、收集器策略都能影响实际晋升,不应把“年龄到某个固定值就一定进入老年代”当成绝对规则。对于应用代码,更有价值的是观察对象分配率、存活率与晋升速度,而不是只记一个默认数字。

JVM 的逃逸分析也与堆分配有关。如果对象没有逃出方法或线程,JIT 可能通过标量替换等优化消除部分分配。源码里出现 new 并不等于每次运行都会产生一个长期堆对象;性能优化应以 JFR、分配剖析或 GC 日志为证据,而不是凭直觉删除对象创建。

堆为什么容易发生 OOM

Java heap space 常见于两大类原因。第一类是内存泄漏:对象已经不再有业务价值,却被静态集合、缓存、监听器、ThreadLocal value 或错误的引用链持续持有。第二类是内存膨胀:短时间创建了大量对象,或者任务、消息、请求结果在无界队列中积压,分配速度超过了回收与堆容量。

GC overhead limit exceeded 表示虚拟机把大量时间花在 GC 上,却回收很少的空间。它是内存压力的信号,不是“GC 本身有 bug”。面对这类错误,应同时看堆占用、对象分配率、存活对象、GC 次数与暂停时间,判断是引用未释放、容量配置不足、还是流量和背压没有边界。

方法区和元空间是什么

方法区是 JVM 规范定义的共享逻辑区域,通常与类结构、字段和方法元数据、运行时常量池等有关。HotSpot 在 JDK 8 移除永久代,改用位于本地内存的 Metaspace 保存类元数据。JDK 7 之后,静态变量和字符串常量池已迁入堆中,因此“方法区里存静态变量与字符串常量池”的说法需要带版本条件。

元空间使用本地内存,默认不再像永久代那样仅受固定 PermGen 上限约束,但它绝非无限。动态代理、字节码增强、脚本引擎、插件重复加载或热部署后旧类加载器没有释放,都可能让类元数据持续增长,最终出现 OutOfMemoryError: Metaspace。此时检查类加载数量、类加载器实例与其引用链,比只看堆对象更关键。

常见参数包括 MetaspaceSizeMaxMetaspaceSize。设置上限可以让问题更早、更可控地暴露,但不能替代修复类加载器泄漏。参数是容量边界,引用关系才是根因。

字符串常量池是什么

字符串常量池用于复用相同内容的字符串,减少重复创建。HotSpot 中它由字符串表等结构维护;随着 JDK 演进,它从永久代移到堆,使得普通垃圾收集可以更及时地处理不再使用的字符串。字面量、编译期常量拼接、运行时 new Stringintern() 的行为并不完全相同,分析时需要看对象是否真的进入池以及谁还持有它。

在业务代码里,不应把 intern() 当作无成本优化。对来自外部输入的大量动态字符串,强行入池可能使生命周期变长,反而增加堆压力。更稳定的优化通常来自减少不必要的复制、限制缓存数量、选择合适的编码与传输协议。

直接内存是什么

直接内存不属于 JVM 规范中的运行时数据区,但 NIO 通过 DirectByteBuffer 可以使用 native 内存。它常用于通道与缓冲区 I/O,目标是减少 Java 堆和 native 缓冲之间的数据复制。直接内存不受 Java 堆上限直接约束,却受到进程总内存、地址空间和相关参数限制,因此也可能导致 OOM。

在容器环境中尤其要注意:-Xmx 只是进程内存的一部分。堆、Metaspace、直接内存、线程栈、JIT 代码缓存、native 库与运行时开销叠加后才是 RSS 的主要来源。若只按堆大小设置容器额度,进程可能在堆还没有满时被系统终止。

Java 对象的创建过程

执行 new 时,虚拟机先确认对应类是否已完成必要的加载与初始化;随后在堆中分配内存,设置对象头、默认零值和类元数据关联,最后执行构造方法。堆空间规整时可采用指针碰撞快速分配;空间不规整时可能要维护空闲列表。多线程分配时,线程本地分配缓冲区可以减少对共享分配指针的竞争。

对象访问常见两种实现思路:句柄访问和直接指针访问。句柄方式让引用先指向稳定句柄,再由句柄定位对象数据和类元数据;对象移动时较容易维护引用。直接指针方式让引用直接指向对象,访问少一次间接跳转。它们是虚拟机实现取舍,应用开发的重点仍是对象生命周期和引用关系。

OOM 发生后如何排查

线上服务应在启动时就配置 -XX:+HeapDumpOnOutOfMemoryError,并将 heap dump 写到有容量、可收集的位置。发生异常后先确认错误类型:heap space、Metaspace、direct buffer memory 和 stack overflow 的排查入口不同。对于堆问题,可使用 MAT 等工具查看 Dominator Tree、对象数量与引用链,找出“谁保留了最多内存”。

同时保留 GC 日志、容器内存限制、活跃线程数、请求量和变更记录。若 heap dump 显示对象数量并不异常,但进程仍被杀死,就要回到堆外内存、线程栈和容器额度;若大对象来自缓存或队列,则要进一步确认淘汰和背压语义。排查的目标不是证明某个参数太小,而是还原对象为什么仍然存在、压力为什么不能被系统吸收。

JVM 内存参数如何理解

堆参数通常从初始大小与最大大小开始理解,常见为 -Xms-Xmx。把二者设成相同值可以减少运行期间扩堆带来的波动,但是否这样设置仍要结合容器额度、应用启动速度与同机资源情况。不要只因“生产环境都应该相等”就照搬;容量规划要预留 Metaspace、直接内存、线程栈和系统开销。

元空间可通过 MetaspaceSizeMaxMetaspaceSize 影响阈值和上限;直接内存的上限也应根据 NIO、网络框架和堆外缓存的实际使用评估。参数调优前先明确目标:是降低频繁 GC、限制异常增长,还是为高峰预留容量。没有 GC 日志、对象分配率和进程内存数据时,单独改参数通常只是在改变故障出现的时间。

堆内存泄漏和内存溢出的区别

内存泄漏强调对象本应不再被使用,却仍被引用链保留;内存溢出强调某一时刻可用空间无法满足新的分配请求。泄漏最终可能导致溢出,但短时间高流量、批量加载、巨型响应或无界任务队列即使不存在传统泄漏,也能触发 OOM。修复泄漏要删除错误引用或建立淘汰机制;修复瞬时膨胀则可能需要限流、分页、分批处理、背压与容量调整。先区分两者,排查才不会走错方向。

面试总结

运行时数据区可以按“线程私有还是共享、存什么、会报什么错”回答。程序计数器负责执行位置,栈负责方法调用,堆负责对象,方法区与 Metaspace 负责类级数据,直接内存服务于堆外 I/O。出现内存问题后,先区分区域,再用 dump、GC 日志和引用链定位;这样才能把 JVM 内存题从名词表变成可执行的排查方法。

FIELD NOTES / DISCUSS

文章讨论

读完后,欢迎留下你的补充、疑问或不同看法。

240 浏览

全部评论 (0)

正在读取评论…

    GUEST IDENTITY

    设置访客身份

    评论、回复与留言板将复用这份身份。

    选择头像