前言
Java 最核心的特性:一次编译,到处运行,底层就是 JVM(Java Virtual Machine,Java 虚拟机)。 我们写的.java源码编译成.class字节码,JVM 加载字节码,解释 / 编译执行。JVM 屏蔽了操作系统底层差异。
学习 JVM,两大核心板块:
- 内存模型(运行时数据区):重点搞懂虚拟机栈、堆
- GC 垃圾回收:什么时候回收、回收什么对象、用什么收集器回收
重要区分:JVM 内存 ≠ 操作系统内存;GC 只回收堆内存中的对象,栈不存在 GC。
一、JVM 运行时数据区
JVM 在执行 Java 程序时,会把管理的内存划分为若干不同区域,部分区域随 JVM 启动创建,JVM 退出销毁;部分区域线程私有,随线程创建销毁。
线程私有区域(每个线程独立一份,互不共享)
- 虚拟机栈(Java 栈)
- 本地方法栈
- 程序计数器
线程共享区域(整个 JVM 一份,所有线程共享)
- 堆(Heap)【GC 主战场】
- 方法区(元空间 MetaSpace,JDK8+)
✅ 一句话区分私有 / 共享:线程私有,线程生则生、线程亡则释放;共享区域由 JVM 统一管理,涉及 GC。
1. 虚拟机栈(重点)
栈:存放局部变量、方法调用栈帧
- 每个 Java 方法执行时,都会创建一个栈帧 (Stack Frame),压入虚拟机栈;方法执行结束,栈帧出栈销毁。
- 栈帧包含:局部变量表、操作数栈、动态链接、方法返回地址。
- 局部变量表存放:基本数据类型(
int/boolean/long)、对象引用地址(不是对象本身!对象本体在堆)。
栈上存的是对象引用,对象实例本身一定在堆。
栈的异常:
StackOverflowError:栈深度超过上限(无限递归)OutOfMemoryError:不断创建线程,耗尽操作系统内存(线程栈分配内存失败)
栈内存没有 GC!方法执行完毕,栈帧自动弹出释放,不需要垃圾回收。
2. 程序计数器
当前线程执行字节码的行号指示器。记录当前执行到哪一行指令。 唯一一个没有 OOM的区域。
3. 本地方法栈
和虚拟机栈作用几乎一致,区别是为native 本地方法(C/C++ 写的方法)服务。
4. 堆 Heap(重中之重,GC 发生在这里)
堆:存放所有对象实例 + 数组,JVM 最大一块内存,所有线程共享,GC 主要回收区域。
JDK8 堆结构划分:
- 新生代(Young Generation):新创建对象,绝大多数对象朝生夕灭。
- Eden 区:对象诞生地,占新生代 80%
- Survivor0(S0,From)、Survivor1(S1,To):各占 10%
同一时间只有一块 Survivor 是可用的,另一块是空的,复制算法。
- 老年代(Old Generation):存活时间久、大对象。多次 GC 存活的对象晋升到老年代。
对象晋升规则:对象在新生代熬过多次 GC(默认 15 次),晋升到老年代;超大对象直接进入老年代,避免 Eden 复制开销。
堆会发生 OOM:
java.lang.OutOfMemoryError: Java heap space,堆内存不足。
5. 方法区(JDK1.8 之后叫元空间 MetaSpace)
存放:类的结构信息、常量、静态变量、即时编译器编译后的代码。
JDK1.7 及之前:永久代(PermGen),属于堆内存一部分,容易 OOM; JDK8:移除永久代,改为元空间,使用操作系统本地内存,不再占用堆内存。
二、栈和堆核心对比(面试必问)
表格
| 对比项 | 虚拟机栈 | 堆 Heap |
|---|---|---|
| 归属 | 线程私有,每个线程独立栈 | 线程共享,全局唯一堆 |
| 存储内容 | 栈帧、局部变量、对象引用地址 | 对象实例、数组 |
| 内存回收 | 自动出栈销毁,无 GC | 由 GC 垃圾收集器回收废弃对象 |
| 大小 | 固定栈深度,一般不大 | JVM 最大内存区域,可通过 - Xmx -Xms 设置 |
| OOM 类型 | StackOverflowError / OOM(创建过多线程) | OOM: Java heap space |
| 生命周期 | 跟随线程 | 跟随 JVM |
高频误区纠正: ❌ 错误说法:对象存在栈里。 ✅ 正确:对象实例一定在堆;栈里面只保存对象的引用地址。 JDK 有逃逸分析,满足条件的小对象会发生栈上分配,属于优化,不是默认行为。面试没特别问逃逸分析,优先按基础结论作答。
三、垃圾回收 GC 基础理论
GC 目标:回收堆中不再被引用的对象,释放内存。
1. 如何判断对象是否可以回收
引用计数法
对象被引用 + 1,引用消失 - 1;计数 = 0 则可回收。 缺点:循环引用无法识别,JVM 没有采用。
可达性分析算法(JVM 采用)
以GC Roots作为起始根节点,向下搜索引用链。不能到达的对象,判定为垃圾,可以回收。
GC Roots 包含:
- 虚拟机栈中局部变量引用的对象
- 方法区静态变量引用对象
- 本地方法栈 native 引用对象
- 锁持有的对象
即使对象不可达,也不是立刻死亡,会执行一次 finalize ()(基本不推荐使用,该方法不可靠)。
2. 三大基础垃圾回收算法
- 标记 - 清除(Mark-Sweep)标记垃圾,统一清除。缺点:内存碎片,分配大对象容易 OOM。
- 复制算法(Copying)新生代使用。Eden + Survivor,存活对象复制到空白 Survivor,清空原区域。无碎片;代价是占用双倍内存。
- 标记 - 整理(Mark-Compact)老年代使用。标记垃圾,清除后,存活对象向一端移动压缩,消除内存碎片。移动对象,STW 时间更长。
新生代:复制算法;老年代:标记清除 / 标记整理。
3. STW Stop The World
GC 执行时,暂停所有用户线程。
所有 GC 收集器都会 STW,差别只是 STW 时间长短。低延迟收集器目标就是减少 STW 时长。
四、主流垃圾收集器(GC 收集器)详解
收集器就是 GC 算法的具体落地实现。不同收集器作用于新生代 / 老年代,搭配使用。
1. Serial 收集器(串行收集器)
- 单线程收集,GC 时唯一 GC 线程工作,所有用户线程 STW。
- 新生代:复制算法;老年代 Serial Old:标记整理。
- 适用:客户端、单机小内存程序。
- 缺点:STW 时间长,大内存场景卡顿明显。
- 参数:
-XX:+UseSerialGC
2. Parallel 收集器(并行收集器,JDK8 默认)
- 多线程 GC,STW,多条 GC 线程并行工作,充分利用多核 CPU。
- Parallel Scavenge(新生代复制) + Parallel Old(老年代标记整理)
- 目标:吞吐量优先(吞吐量 = 用户运行时间 / 总运行时间)。牺牲停顿时间,追求最大吞吐量。
- 适合:后台任务、批量计算,对停顿不敏感的服务。
- 参数:
-XX:+UseParallelGC
3. CMS 收集器(Concurrent Mark Sweep,并发标记清除)
JDK1.5 推出,JDK9 开始废弃,JDK14 移除
- 老年代收集器,并发标记 + 并发清除,尽量和用户线程一起跑,降低 STW。
- 算法:标记清除,会产生内存碎片。
- 目标:低延迟,减少停顿时间
- 执行四阶段:
- 初始标记(STW,很短)
- 并发标记(用户线程同时运行)
- 重新标记(STW)
- 并发清除(用户线程同时运行)
- 缺点:内存碎片、CPU 占用高、浮动垃圾,无法处理老年代快速增长。
4. G1 收集器(Garbage-First,JDK9 默认)
面向服务端,吞吐量与低延迟之间平衡,JDK8 推荐,JDK9 默认 核心思想:化整为零,将整个堆划分为多个大小相等的 Region 区域,Eden、Survivor、老年代不再连续。
- 可以同时管理新生代 + 老年代。
- 优先回收垃圾最多的 Region(Garbage First)。
- 算法:标记整理 + 复制,不会产生大量内存碎片
- 支持可预测停顿,可设置预期最大停顿时间
-XX:MaxGCPauseMillis - 阶段:初始标记 (STW)、并发标记、最终标记 (STW)、筛选回收 (STW)
- 参数:
-XX:+UseG1GC
5. ZGC(Z Garbage Collector,JDK11 引入,JDK17 稳定)
超低延迟收集器,大内存首选
- 几乎可以做到亚毫秒级 STW,堆可以支持 TB 级别。
- 着色指针 + 读屏障实现并发标记、并发整理。
- 目标:STW 时间几乎不随堆内存变大而增长。
- 适用:高并发、低延迟业务,网关、订单核心服务。
- 参数:
-XX:+UseZGC
6. Shenandoah 收集器
RedHat 开发,和 ZGC 类似,低延迟,并发整理,JDK12 引入。国内使用少于 ZGC。
五、GC 收集器对比汇总表
表格
| 收集器 | 作用区域 | 算法 | 特点 | 目标场景 |
|---|---|---|---|---|
| Serial | 新 + 老 | 复制 / 标记整理 | 单线程,STW 长 | 小应用,客户端 |
| Parallel | 新 + 老 | 复制 / 标记整理 | 多线程,吞吐量优先 | 后台批量任务,JDK8 默认 |
| CMS | 老年代 | 标记清除 | 并发,低延迟,内存碎片 | JDK8 老项目,现已淘汰 |
| G1 | 整个堆 | 标记整理 + 复制 | 分区模型,可控停顿,无碎片 | 中小型服务,JDK9 默认 |
| ZGC | 整个堆 | 并发标记整理 | 亚毫秒级 STW,支持超大堆 | 高并发核心业务,低延迟 |
六、JVM 启动常用参数
# 堆初始大小,最大堆 -Xms2g -Xmx2g # 指定GC收集器示例 -XX:+UseG1GC -XX:+UseZGC # 打印GC日志 -XX:+PrintGCDetails七、面试高频考点
- 堆和栈存放什么?对象在哪?
栈存放局部变量、对象引用;堆存放对象实例;栈不存在 GC,堆由 GC 回收。
- 可达性分析是什么,GC Roots 有哪些?
从 GC Roots 向下遍历引用链,不可达对象判定垃圾;列出 4 类 GC Roots。
- CMS、G1、ZGC 核心区别?
CMS:老年代,标记清除,有碎片;G1 分区化,平衡吞吐量和延迟;ZGC 超低 STW,适合大堆低延迟场景。
- STW 是什么?所有 GC 都会 STW 吗?
用户线程暂停;全部收集器都有 STW,只是停顿长短不同。
- 新生代 GC(Minor GC)、老年代 GC(Full GC)
Minor GC:新生代满触发,频率高,速度快;Full GC:整堆回收,STW 时间很长,尽量避免。
补充:Major GC 指老年代 GC,经常伴随 Full GC。
八、总结
- 虚拟机栈是线程私有,存引用,方法结束自动释放,没有 GC;堆是共享区域,存放对象实例,GC 回收堆内存。
- JVM 使用可达性分析判定垃圾,三大基础 GC 算法:复制、标记清除、标记整理。
- Parallel:吞吐量优先;CMS(淘汰)追求低延迟,有碎片;G1 分区,平衡方案;ZGC 亚毫秒级低延迟,现代业务首选。
- 线上调优核心目标:尽量减少 Full GC,降低 STW 停顿。