一、 为什么需要GC?基础理论概览

在C/C++等语言中,内存管理需要开发者手动分配和释放,这不仅繁琐而且极易导致内存泄漏或悬空指针。Java引入了自动内存管理系统,即垃圾回收(Garbage Collection, GC)。面试官常问:“GC的作用是什么?其核心目标是识别不再使用的对象并回收其占用的内存空间,从而防止内存泄漏,提高程序的稳定性和生命周期。”

1.1 对象存活判断算法

在面试中,判断哪些对象需要被回收是核心考点。主要有两种算法:

① 引用计数法 (Reference Counting)

原理:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;引用失效时,计数器值减1。任何时刻计数器为0的对象就是不可能再被使用的。

⚠️ 缺点:无法解决对象之间的循环引用问题(例如A引用B,B引用A,即使它们不再被外部使用,计数器也不为0)。因此,HotSpot虚拟机没有采用此算法。

② 可达性分析算法 (Reachability Analysis)

原理:通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。

✅ 优势:解决了循环引用问题,是主流JVM实现的标准算法。

1.2 哪些对象可以作为 GC Roots?

  • ① 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • ② 方法区中类静态属性引用的对象。
  • ③ 方法区中常量引用的对象。
  • ④ 本地方法栈中JNI(即一般说的Native方法)引用的对象。

二、 JVM 内存模型与区域划分

理解内存区域是理解GC的前提。JVM运行时数据区主要包括以下几个部分,其中堆(Heap)和方法区(Method Area/Metaspace)是GC的主要发生地。

内存区域 线程共享/私有 生命周期 主要用途 常见GC区域
堆 (Heap) 共享 与JVM生命周期一致 存放对象实例,是GC的重点区域 是(Young/Old Gen)
虚拟机栈 (VM Stack) 私有 与线程生命周期一致 存储栈帧(局部变量、操作数栈等) 否
本地方法栈 (Native Method Stack) 私有 与线程生命周期一致 为Native方法服务 否
方法区 (Method Area) 共享 与JVM生命周期一致 存储已被虚拟机加载的类信息、常量、静态变量 是(Metaspace)
程序计数器 (Program Counter Register) 私有 与线程生命周期一致 当前线程所执行的字节码行号指示器 否

2.1 堆内存细分

堆内存是GC最核心的区域,通常分为新生代(Young Generation)和老年代(Old Generation)。

  • 新生代又分为:Eden区、S0(Survivor 0)区、S1(Survivor 1)区。默认比例为 8:1:1。
  • 对象首先在Eden区分配,当Eden区满时,触发Minor GC。
  • 经过Minor GC后仍存活的对象,会被移动到Survivor区。随着对象年龄增长,达到一定阈值(默认15岁)后,对象会被晋升到老年代。

三、 主流垃圾收集器对比与选择

面试中经常考察不同收集器的特点、适用场景以及它们之间的组合。JVM提供了多种收集器,它们之间的主要区别在于内存回收算法、收集线程数以及是否产生内存碎片。

CMS (Concurrent Mark Sweep)

核心特点:以获取最短回收停顿时间为目标的收集器。适用于注重服务响应的场景(如Web应用)。

工作流程:初始标记 → 并发标记 → 重新标记 → 并发清除。

优点:并发收集,用户线程停顿时间短。

缺点:
1. 产生内存碎片(标记-清除算法导致)。
2. 对CPU资源敏感。
3. 无法处理浮动垃圾(Concurrent Mode Failure)。

// JVM参数示例
-XX:+UseConcMarkSweepGC

G1 (Garbage-First)

核心特点:JDK 9 之后的默认收集器。将堆划分为多个独立的区域(Region),不再物理隔离新生代和老年代。

工作流程:初始标记 → 并发标记 → 最终标记 → 筛选回收。

优点:
1. 可以精确控制停顿时间(-XX:MaxGCPauseMillis)。
2. 整合了CMS和Parallel Old的优点,无内存碎片。
3. 适合大内存机器(多核处理器)。

// JVM参数示例
-XX:+UseG1GC

ZGC (Z Garbage Collector)

核心特点:JDK 11 引入的实验性收集器,JDK 15 成为正式特性。目标是支持TB级堆内存,且停顿时间不超过10ms。

核心技术:
1. 读屏障(Load Barriers):在对象访问时检查颜色位。
2. 染色指针(Colored Pointers):在指针中存储状态信息。
3. 并发重定位:在标记阶段同时移动对象。

优点:极低停顿时间,几乎不受堆大小影响。

// JVM参数示例 (JDK 11+)
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

3.1 收集器组合矩阵

新生代收集器 老年代收集器 组合名称 适用场景
Parallel Scavenge Parallel Old Parallel Scavenge + Parallel Old 吞吐量优先(科学计算、大数据处理)
Serial / ParNew CMS ParNew + CMS + Parallel Scavenge 响应时间优先(Web应用,JDK8默认组合之一)
G1 G1 G1 大内存、低停顿需求(JDK9+默认)

四、 GC 调优实战与故障排查

面试不仅考察理论,更考察解决实际问题的能力。常见的GC问题包括:Full GC频繁、内存泄漏、停顿时间过长。

4.1 常见GC日志分析

开启GC日志参数:-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps

示例日志片段:

[2023-10-27T10:00:00.000+0800]: [GC (Allocation Failure) [PSYoungGen: 102400K->768K(131072K)] 102400K->9216K(458752K), 0.0050000 secs]
[2023-10-27T10:00:00.005+0800]: [Full GC (Ergonomics) [PSYoungGen: 768K->0K(131072K)] [ParOldGen: 8192K->9216K(327680K)] 8960K->9216K(458752K), [Metaspace: 10240K->10240K(1064960K)], 0.1000000 secs]

解读:

  • Allocation Failure:新生代空间不足,触发Minor GC。
  • PSYoungGen: 102400K->768K(131072K):GC前占用102400K,GC后占用768K,总容量131072K。
  • Full GC:老年代空间不足或System.gc()触发,导致Stop The World,停顿时间0.1秒,较长。

4.2 调优策略建议

第一步
监控与识别

使用JVM监控工具(如JConsole, VisualVM, Arthas)观察GC频率、停顿时间及内存趋势。识别是Minor GC频繁还是Full GC频繁。

第二步
调整堆大小

如果Minor GC频繁,尝试增大新生代大小(-Xmn)。如果Full GC频繁,检查是否老年代增长过快,适当增大老年代空间(-Xms, -Xmx)。

第三步
代码级优化

排查内存泄漏:检查是否存在ThreadLocal未移除、大集合未清理、静态集合无限增长等问题。避免在循环中创建大对象。

第四步
更换收集器

如果当前收集器无法满足停顿时间要求,考虑从CMS切换到G1或ZGC。

五、 面试高频问答 (FAQ)

Q1: Minor GC 和 Full GC 有什么区别?

Minor GC仅回收新生代,速度较快;Full GC回收整个堆和方法区,速度较慢,会引发长时间的Stop The World。通常Full GC是由老年代空间不足、元空间不足或System.gc()调用触发的。

Q2: 什么是 Stop The World (STW)?

指在GC执行期间,除了GC线程外,其他所有应用线程都被暂停。STW时间越短,用户体验越好。G1和ZGC通过并发处理大幅减少了STW时间。

Q3: 如何判断是否发生了内存泄漏?

如果Full GC后堆内存没有明显下降,且随着时间推移内存占用持续增长,直至OutOfMemoryError,则很可能存在内存泄漏。可使用MAT或JProfiler分析Heap Dump。

Q4: CMS收集器的浮动垃圾是什么?

CMS在并发清理阶段,用户线程仍在运行,可能会产生新的垃圾对象。这些对象无法在本次GC中被清理,被称为浮动垃圾。如果浮动垃圾过多,可能导致后续Full GC。

网友们还关心

除了基础的GC机制,求职者通常还会关注以下与面试gc垃圾回收机制紧密相关的周边知识:

推荐阅读:强软弱虚引用详解

在GC中,引用类型的强弱直接影响对象是否被回收:

  • 强引用 (Strong Reference):如 Object obj = new Object(),只要强引用存在,对象永远不会被GC回收。
  • 软引用 (Soft Reference):内存充足时不回收,内存不足时回收。常用于缓存实现。
  • 弱引用 (Weak Reference):只要发生GC,无论内存是否充足,都会被回收。如 WeakHashMap。
  • 虚引用 (Phantom Reference):无法通过虚引用获取对象,主要用于跟踪对象被GC回收的活动,配合引用队列使用。