一、为什么需要垃圾回收(GC)
- Java 程序运行过程中会不断创建对象,堆内存有限。
- 无法被引用的对象占用空间,如果不释放,会导致 内存泄漏或内存溢出(OOM)。
- 手动管理内存易出错,Java 引入自动垃圾回收机制。
二、JVM 中的内存区域(重点)
区域 | 说明 |
---|---|
程序计数器 | 每个线程私有,记录字节码执行位置 |
虚拟机栈 | 每个线程私有,保存局部变量表 |
本地方法栈 | 调用 native 方法时使用 |
堆(Heap) | 所有线程共享,垃圾收集的主要区域,保存对象实例 |
方法区(元空间) | 保存类信息、常量、静态变量等 |
三、如何判断对象是否可以被回收?
引用计数法(Reference Counting)
- 每个对象维护一个引用计数器;
- 计数为 0 → 可回收;
- 缺陷:无法处理循环引用。
可达性分析法(Reachability Analysis)(Java 使用)
从 GC Root 出发向外遍历:
- 方法区静态变量
- 方法栈局部变量
- JNI 引用
如果一个对象无法从 GC Root 访问到,则被判定为不可达 → 可回收对象。
四、对象的生命周期与回收区域
区域 | 描述 |
---|---|
新生代(Young Generation) | 包括 Eden + 两个 Survivor 区域(S0、S1) |
老年代(Old Generation) | 存放长时间存活的对象 |
元空间(Metaspace) | Java 8 后替代永久代,存类元数据 |
五、常见垃圾回收算法
复制算法(Copying)
应用于新生代;
将 Eden 中的存活对象复制到 Survivor 区;
一般为 Eden + From(S0)+ To(S1):
- 每次使用其中两个,另一个为空;
优点:没有碎片,效率高;
缺点:内存浪费严重(只用了一半)。
标记-清除算法(Mark-Sweep)
应用于老年代;
第一步:标记可达对象;
第二步:清除未被标记对象;
缺点:
- 产生内存碎片;
- 清除速度慢。
标记-压缩(整理)算法(Mark-Compact)
- 是标记清除的改进;
- 标记完后把存活对象压缩到一端,释放内存;
- 避免内存碎片问题,适用于老年代。
分代收集(Generational GC)
新生代使用复制算法,老年代使用标记清除或压缩算法;
理由:
- 大多数对象“朝生夕死”,适合复制算法;
- 老年代对象存活率高,复制开销大,适合标记压缩。
六、常见垃圾收集器
收集器名称 | 作用区域 | 特点 |
---|---|---|
Serial | 新生代 | 单线程,Stop-The-World,适合单核/小应用 |
ParNew | 新生代 | Serial 的多线程版本 |
Parallel Scavenge | 新生代 | 吞吐量优先,适合后台批处理系统 |
CMS(已废弃) | 老年代 | 并发收集、低停顿,但容易产生碎片 |
Serial Old | 老年代 | Serial 的老年代版本 |
Parallel Old | 老年代 | Parallel 的老年代版本 |
G1(推荐) | 新+老 | 面向服务端,低延迟、可预测停顿 |
ZGC / Shenandoah | 新+老 | 超低延迟,适合超大内存(100GB+)应用 |
七、GC 类型与触发时机
GC 类型 | 触发时机 | 回收区域 | 使用算法 |
---|---|---|---|
Minor GC | 新生代满 | 新生代 | 复制算法 |
Major GC | 老年代满 | 老年代 | 标记-压缩 |
Full GC | 整体内存压力高、元空间满、System.gc() | 新生代 + 老年代 + 元空间 | 综合算法 |
八、如何选择垃圾收集器?
单核、资源紧张、客户端程序:
- 使用 Serial + Serial Old
多核、后台任务型应用、对吞吐量要求高:
- 使用 Parallel Scavenge + Parallel Old
用户体验要求高、低延迟应用:
- 使用 G1(JDK9 后默认)、ZGC、Shenandoah
九、G1 收集器简要说明(面试重点)
- 将堆划分为多个 Region;
- 并发执行 Mark 阶段,减少 STW;
- 优先回收垃圾最多的 Region(Garbage First);
- 可通过
-XX:MaxGCPauseMillis
控制最大停顿时间; - 对大内存友好,JDK9+ 推荐默认使用。
十、调试与配置常用参数
# 查看 GC 日志(JDK8)
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
# 设置新生代大小
-Xmn256m
# 设置堆最大/最小值
-Xms512m -Xmx512m
# 使用 G1 收集器
-XX:+UseG1GC
# 控制最大 GC 停顿时间
-XX:MaxGCPauseMillis=200
十一、垃圾回收相关工具
工具名称 | 说明 |
---|---|
jstat |
查看 JVM 各种运行指标(GC、类加载、堆等) |
jvisualvm |
图形化 JVM 监控工具 |
jconsole |
图形化管理工具,可连远程 |
GCViewer |
分析 GC 日志 |
MAT (Memory Analyzer Tool) |
堆内存分析工具,找内存泄漏 |
十二、面试高频问题总结
Q1:对象从新生代晋升到老年代的条件?
- 经历
-XX:MaxTenuringThreshold
次 GC; - Survivor 区放不下时,直接进入老年代;
- Survivor 区年龄分布达到阈值,提前晋升。
Q2:Minor GC 和 Full GC 有什么区别?
- Minor GC 只回收新生代,频率高,速度快;
- Full GC 回收整个堆(新生代 + 老年代 + 元空间),频率低,STW 时间长,影响性能。
Q3:如何判断是否发生内存泄漏?
- GC 后对象仍然没有被释放;
- 使用
jmap + MAT
工具分析对象引用链。
总结
JVM 垃圾回收的核心是“分代 + 区域 + 策略”,通过合适的算法(复制、标记清除、压缩)和收集器(如 G1、ZGC)实现不同场景下的高效内存管理,是高性能 Java 应用的保障。
十三、三色标记法(Tri-Color Marking)
三色标记法是什么?
三色标记法 是现代垃圾回收器中用于并发标记阶段的一种对象可达性追踪算法。其核心目的是在应用线程运行过程中,仍能安全、准确地标记存活对象,避免漏标或误回收。
它被广泛应用于:
- CMS
- G1
- ZGC
- Shenandoah 等收集器的并发 GC 实现中
三种颜色含义
颜色 | 状态说明 |
---|---|
白色 | 初始状态,表示对象未被访问;最终仍为白的对象将被回收 |
灰色 | 对象已被访问,但其引用的其他对象尚未全部扫描完毕 |
黑色 | 对象已访问完毕,自身和引用对象都已处理,判定为存活 |
标记流程
所有对象初始为白色;
GC Root 集合作为入口,加入灰色集合;
循环处理灰色对象:
- 标记为黑;
- 扫描其引用的白色对象 → 转为灰色;
灰色集合为空后:
- 剩余未访问的白色对象即为不可达对象 → 回收。
并发 GC 中的“漏标”问题
在并发标记时,用户线程仍在运行,可能会导致以下情况:
- 对象 A 被标记为黑;
- A 指向对象 B(白色);
- 此时用户线程将 A → B 的引用断开,并把 C(灰)指向 B;
- 后续 GC 先处理了 A(黑),再处理 C(灰)时不再追踪 B;
- B 由于“断开”后未再被扫描,最终仍为白色 → 被错误回收(漏标)!
解决办法
现代 GC 通常通过以下机制解决漏标问题:
技术 | 描述说明 |
---|---|
增量更新(Incremental Update) | 当黑对象新增引用白对象时,重新将白对象置为灰色,等待重新扫描 |
SATB(Snapshot At The Beginning) | 保留标记开始时的对象引用快照,即使中途引用断了,也按初始快照追踪对象存活性 |
应用收集器
GC 收集器 | 是否使用三色标记法 | 附加机制 |
---|---|---|
CMS | 是 | 增量更新 |
G1 | 是 | 增量更新 |
ZGC | 是 | SATB |
Shenandoah | 是 | SATB |
三色标记法通过黑 / 灰 / 白三种状态动态追踪对象引用,确保在并发标记时依然准确地识别出所有存活对象,是现代低停顿 GC 的基础核心算法。