JVM内存模型决定了对象创建后落在哪块内存、能被谁访问,垃圾回收机制决定了这些内存何时被回收、以多快速度回收把这两件事搞清楚,Java程序的内存溢出、频繁Full GC、响应抖动这些问题就有了排查的抓手。
Java程序员写代码时几乎不用手动malloc和free,这份省心背后全靠JVM撑着,它一边把内存划分成职责明确的几块区域,一边在后台悄悄清理不再使用的对象,理解这两套机制,不是应付面试,而是线上出问题时能快速定位的底气。
JVM内存模型到底由哪几块组成
JVM在运行时会把自己管理的内存切成若干区域,划分依据是线程共享还是线程私有。
线程共享区:堆与方法区
堆是JVM中最大的一块内存,几乎所有的对象实例和数组都在这里分配,堆内部继续分代:新生代和老年代,新生代又细分为Eden、Survivor From、Survivor To,默认比例大致为8:1:1,这个分代设计的出发点是绝大多数对象朝生夕死,把短命对象和长命对象分开管理,回收效率更高。
方法区存放类的元信息、常量、静态变量等,JDK 8是一个分水岭:之前叫永久代,占用JVM堆内存;之后改为元空间,直接使用本地内存,默认不设上限,受物理内存约束。
线程私有区:虚拟机栈、本地方法栈、程序计数器
- 虚拟机栈:每个线程一份,方法调用时压入栈帧,栈帧里装着局部变量表、操作数栈、返回地址,栈深度超限抛StackOverflowError。
- 本地方法栈:为native方法服务,结构和虚拟机栈类似。
- 程序计数器:记录当前线程执行到哪条字节码,是唯一不会抛OutOfMemoryError的区域。
一张表看清各区域差异
| 区域 | 线程共享 | 主要存放内容 | 典型异常 |
|---|---|---|---|
| 堆 | 是 | 对象实例、数组 | OutOfMemoryError |
| 方法区/元空间 | 是 | 类元信息、常量、静态变量 | OutOfMemoryError |
| 虚拟机栈 | 否 | 栈帧、局部变量 | StackOverflowError |
| 本地方法栈 | 否 | native方法栈帧 | StackOverflowError |
| 程序计数器 | 否 | 字节码行号 | 无 |
Java面试JVM内存模型常见问题拆解
面试里这块问得细,但真正拉开差距的,是能否把概念和线上现象对上号。
栈内存和堆内存到底差在哪
栈的生命周期跟着线程走,方法结束栈帧就弹出,由系统自动管理;堆的生命周期跟着对象走,什么时候回收由GC决定,栈上分配速度快、容量小;堆容量大,但分配和回收都有成本,逃逸分析开启后,如果对象没有逃出方法作用域,JIT会把它的分配从堆挪到栈,这就是标量替换和栈上分配优化。
方法区、永久代、元空间三者什么关系
方法区是规范里的概念,永久代和元空间是不同JDK版本对它的实现,JDK 7把字符串常量池移出永久代,JDK 8彻底用元空间替换永久代,元空间用本地内存,好处是类加载太多时不再直接拖垮堆,但如果不限制大小,也可能把机器内存吃光。
哪些对象会从新生代晋升到老年代
- 对象在Survivor区每熬过一次Minor GC,年龄加1,默认到15岁晋升老年代。
- 大对象直接进老年代,避免在Eden和Survivor之间反复复制。
- Survivor区中相同年龄对象总和超过一半,该年龄及以上的直接晋升。
- 动态年龄判定,由虚拟机根据实际情况决定。
垃圾回收机制靠什么判断对象该被回收
判断对象是否存活,是GC的第一步。
引用计数法为什么被淘汰
引用计数给对象加一个计数器,有引用加1,引用失效减1,归零就回收,它实现简单、判断及时,但无法处理循环引用两个对象互相引用,计数永远不归零,主流JVM没有采用它作为最终判断依据。
可达性分析与GC Roots
行业共识认为,可达性分析是判断对象存活的通用方案,思路是从一组称为GC Roots的根对象出发向下搜索,能走到的对象标记为存活,走不到的就可以回收,常见的GC Roots包括:
- 虚拟机栈中局部变量表引用的对象
- 方法区中静态变量引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI引用的对象
- 活跃线程对象
四种引用类型对回收的影响
- 强引用:只要还挂着,GC绝不回收。
- 软引用:内存不够时才回收,适合做缓存。
- 弱引用:下次GC一定回收,ThreadLocal的Entry用的就是它。
- 虚引用:唯一用途是对象被回收时收到通知,配合ReferenceQueue使用。

JVM垃圾回收器CMS和G1有什么区别
这是被问得最多的一组对比,两者设计目标不同,适用场景也不同。
CMS:以最短停顿为目标的并发收集器
CMS把回收拆成四个阶段:初始标记、并发标记、重新标记、并发清除,初始标记和重新标记需要暂停用户线程,并发阶段可以和业务线程一起跑,它的短板是标记-清除会产生内存碎片,而且并发阶段会占用CPU,容易触发Concurrent Mode Failure,进而退化成Serial Old全停顿回收。
G1:面向大堆的分区化收集器
G1把堆拆成大小相等的Region,不再物理上连续分代,它通过维护每个Region的回收价值,优先回收垃圾最多的区域,因此得名Garbage First,停顿时间可以设定目标值,比如-XX:MaxGCPauseMillis=200,G1会尽量在这个范围内完成回收,整体基于标记-复制算法,碎片问题比CMS轻。
主流收集器横向对比
| 收集器 | 适用场景 | 算法 | 是否可设停顿目标 |
|---|---|---|---|
| Serial | 单核小堆、客户端模式 | 复制/标记-整理 | 否 |
| Parallel | 吞吐量优先、批处理 | 复制/标记-整理 | 否 |
| CMS | 低延迟、JDK 9前常用 | 标记-清除 | 否 |
| G1 | 大堆、低延迟、JDK 9后默认 | 分区复制 | 是 |
| ZGC | 超大堆、极低停顿 | 染色指针+读屏障 | 是 |
生产环境怎么选
堆小于4G且追求吞吐,Parallel够用,堆在6G到几十G,又在意响应时间,G1是默认答案,堆超过百G且停顿要求苛刻,再看ZGC或Shenandoah,近年来不少中间件把默认收集器切到G1,正是看中它可预测的停顿表现。
线上服务JVM调优实战步骤
调优的前提是先拿到数据,别上来就改参数。
第一步:摸清运行时状态
jps -l # 查看Java进程PID jstat -gcutil <pid> 1000 10 # 每秒打印一次GC统计 jmap -histo:live <pid> | head -30 # 查看存活对象直方图 jstack <pid> > stack.log # 导出线程栈

重点关注jstat -gcutil输出里的FGC次数、FGCT总耗时、老年代使用率,如果FGC频繁且每次回收后老年代占用降不下来,多半是内存泄漏而非参数问题。
JVM堆内存溢出怎么排查
先确认是哪种OOM:
- 堆内存溢出,在启动参数加
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump,拿到hprof文件。 - 用MAT或VisualVM打开dump,看支配树里哪个对象占用最大。
- 顺着引用链找到持有它的GC Roots,定位到具体代码。
- 元空间溢出则检查是否动态生成类过多,比如反射、CGLIB代理滥用。
- 线程数溢出检查线程池配置和是否有阻塞任务堆积。
常用调优参数参考
-Xms与-Xmx设成相同值,避免堆反复伸缩。-Xmn控制新生代大小,官方建议占堆的1/3到1/2。-XX:SurvivorRatio=8设定Eden与单个Survivor的比例。-XX:+UseG1GC -XX:MaxGCPauseMillis=200启用G1并设停顿目标。-XX:MetaspaceSize和-XX:MaxMetaspaceSize给元空间设上下限,防止无限膨胀。
关于JVM内存模型与垃圾回收机制的常见问答
Q:JDK 8之后字符串常量池到底在哪?
A:JDK 7就把字符串常量池从永久代挪到了堆里,JDK 8用元空间替换永久代后,字符串常量池依然留在堆中,所以大量intern操作会直接反映在堆内存占用上。
Q:Minor GC和Full GC的触发条件分别是什么?
A:Minor GC在Eden区满时触发,回收新生代,Full GC触发条件更多:老年代空间不足、元空间扩容、显式调用System.gc()、CMS的Concurrent Mode Failure等,业内专家指出,频繁Full GC通常意味着对象晋升过快或存在内存泄漏。
Q:G1适合多大的堆,小堆用G1会亏吗?
A:G1设计目标是大堆低延迟,一般建议堆在6G以上再考虑,堆太小的时候,Region数量少,G1的分区管理开销反而显得冗余,Parallel或Serial在几G以内的堆上吞吐表现更实在,据OpenJDK官方文档,G1在JDK 9之后成为服务端默认收集器,但这不等于所有场景都优于其他收集器。
JVM内存模型划定了数据存放的边界,垃圾回收机制决定了这些数据的生命周期,把分代结构、可达性分析、收集器特性三块拼在一起,线上遇到GC问题时,才有一张清晰的排查地图可循。
