Java 垃圾回收
Java 垃圾回收(Garbage Collection, GC)机制是 Java 虚拟机(JVM)的核心功能之一,负责自动管理内存。以下是对其详细的解释:
一、Java 内存区域划分
Java 堆内存主要分为三个区域:
-
新生代(Young Generation)
- 新创建的对象优先分配在此区域。
- 分为三个部分:
- Eden 区:对象初始分配的区域。
- Survivor 区(S0 和 S1):Eden 区满时,存活的对象被移至 Survivor 区。两个 Survivor 区始终有一个为空,用于复制算法。
-
老年代(Old Generation)
- 存放长期存活的对象(如缓存对象、静态变量引用的对象)。
- 当对象在 Survivor 区经历多次 GC 后仍存活(默认 15 次),会被晋升到老年代。
-
永久代 / 元空间(PermGen/Metaspace)
- 永久代(Java 7 及以前):存储类信息、常量池等。
- 元空间(Java 8 及以后):使用本地内存,避免永久代的内存溢出问题。
二、垃圾回收的核心概念
-
可达性分析(Reachability Analysis)
- 从GC Roots(如静态变量、栈帧中的引用、本地方法栈中的引用等)出发,遍历对象引用链。未被引用的对象被标记为不可达,可被回收。
-
引用类型
- 强引用:
Object obj = new Object()
,只要强引用存在,对象就不会被回收。 - 软引用(SoftReference):内存不足时会被回收,常用于缓存。
- 弱引用(WeakReference):下次 GC 时立即回收,如
WeakHashMap
。 - 虚引用(PhantomReference):仅在对象被回收时收到通知,用于管理堆外内存。
- 强引用:
-
垃圾回收算法
- 标记 - 清除(Mark-Sweep):标记可达对象,清除未标记对象,会产生内存碎片。
- 标记 - 整理(Mark-Compact):标记后将存活对象移动到一端,避免碎片。
- 复制(Copying):将内存分为两块,每次只使用一块,GC 时将存活对象复制到另一块,效率高但空间利用率低。
- 分代收集(Generational Collection):结合上述算法,新生代用复制算法,老年代用标记 - 清除或标记 - 整理。
三、对象从新生代到老年代的晋升过程
-
对象创建
- 新对象首先分配在 Eden 区。若 Eden 区空间不足,触发Minor GC(新生代 GC)。
-
第一次 Minor GC
- 存活的对象被移至Survivor 区(S0 或 S1),并将对象年龄(GC 次数)设为 1。
- Eden 区被清空。
-
后续 Minor GC
- 每次 Minor GC 后,存活的对象在两个 Survivor 区之间来回复制,年龄递增。
- 当对象年龄达到阈值(默认 15 次),晋升到老年代。
-
动态对象年龄判定
- 若 Survivor 区中相同年龄的对象大小总和超过 Survivor 区的一半,年龄大于或等于该年龄的对象直接晋升到老年代。
-
大对象直接进入老年代
- 超过
-XX:PretenureSizeThreshold
参数设置的大对象(如长数组),直接分配到老年代,避免在 Eden 区和 Survivor 区之间复制。
- 超过
-
空间分配担保
- 老年代需确保有足够空间容纳新生代晋升的对象。若老年代空间不足,触发Full GC(整堆 GC)。
四、垃圾回收器类型
不同的垃圾回收器适用于不同场景,JVM 会根据配置和硬件自动选择:
-
新生代回收器
- Serial:单线程,适合客户端应用。
- ParNew:Serial 的多线程版本,与 CMS 配合使用。
- Parallel Scavenge:多线程,注重吞吐量(CPU 用于运行代码的时间比例)。
-
老年代回收器
- Serial Old:单线程,用于 Client 模式或与 CMS 配合。
- Parallel Old:多线程,与 Parallel Scavenge 配合,注重吞吐量。
- CMS(Concurrent Mark Sweep):以获取最短停顿时间为目标,并发标记和清除,但会产生碎片。
- G1(Garbage-First):分代收集,将堆划分为多个 Region,优先回收垃圾最多的区域。
- ZGC(Z Garbage Collector):超低延迟,支持 TB 级内存,适用于大内存应用。
-
全区域回收器
- Shenandoah:与 G1 类似,但更注重低延迟。
- Epsilon:实验性回收器,仅分配内存,不执行 GC,用于性能测试。
五、GC 触发条件
- Minor GC:Eden 区空间不足时触发。
- Full GC:
- 老年代空间不足(如晋升对象大小超过老年代剩余空间)。
- 元空间 / 永久代满(如动态加载大量类)。
- 显式调用
System.gc()
(可能被 JVM 忽略)。 - CMS GC 时并发模式失败(Concurrent Mode Failure)。
六、GC 性能调优要点
-
监控工具
jstat
:查看 GC 统计信息。jmap
:生成堆转储文件。jhat
/MAT
:分析堆转储文件。G1GCViewer
/GCeasy
:可视化 GC 日志。
-
常用参数
-Xms
/-Xmx
:初始 / 最大堆内存。-Xmn
:新生代大小。-XX:SurvivorRatio
:Eden 区与 Survivor 区的比例。-XX:MaxTenuringThreshold
:晋升老年代的年龄阈值。-XX:+UseG1GC
:启用 G1 回收器。
-
优化方向
- 减少 Full GC 频率,避免大对象和内存泄漏。
- 根据应用特性选择回收器(如 Web 应用优先用 CMS/G1,科学计算优先用 Parallel)。
- 调整新生代大小,平衡 Minor GC 和晋升频率。
七、示例:对象晋升流程
假设 JVM 配置为:
- 堆内存:2GB
- 新生代:512MB(Eden: 400MB,S0: 56MB,S1: 56MB)
- 老年代:1.5GB
- 晋升阈值:15 次
流程:
- 新对象(100MB)分配在 Eden 区。
- Eden 区占满 400MB 时,触发 Minor GC。
- 存活的对象(200MB)被复制到 S0,对象年龄设为 1。
- 下次 Minor GC 时,Eden 区和 S0 的存活对象(150MB)被复制到 S1,年龄 + 1。
- 当对象年龄达到 15 次,或 Survivor 区空间不足时,对象晋升到老年代。
面试题
1. 什么是 Java 垃圾回收机制?为什么需要它?
Java GC 是一种自动内存管理机制,由 JVM 负责回收不再使用的对象占用的内存。它的主要作用是:
- 避免手动内存管理导致的内存泄漏和悬空指针问题。
- 提高开发效率,让程序员专注于业务逻辑。
- 通过分代回收和内存整理,优化内存利用率。
2. 如何判断一个对象是否可以被回收?
JVM 使用 ** 可达性分析(Reachability Analysis)** 算法:
- 从 GC Roots(如静态变量、栈帧中的引用、本地方法栈中的引用等)出发,遍历对象引用链。
- 不可达的对象被标记为可回收(如无任何引用指向的对象)。
- 引用类型(强、软、弱、虚)会影响对象的回收优先级。
3. 简述 Java 堆内存的分区及各区域的特点。
Java 堆分为:
- 新生代(Young Generation):
- 新对象优先分配在此区域,分为 Eden 区和两个 Survivor 区(S0、S1)。
- 频繁触发 Minor GC,使用复制算法。
- 老年代(Old Generation):
- 存放长期存活的对象(如经历多次 Minor GC 后仍存活的对象)。
- 触发 Full GC 的频率较低,使用标记 - 清除或标记 - 整理算法。
- 元空间 / 永久代(Metaspace/PermGen):
- 存储类信息、常量池等,Java 8 后使用元空间(本地内存)替代永久代。
4. 对象从新生代到老年代的晋升过程是怎样的?
- 对象创建:新对象分配在 Eden 区。
- Minor GC:Eden 区满时触发,存活对象移至 Survivor 区(如 S0),年龄设为 1。
- 后续 GC:每次 GC 后存活对象在 Survivor 区之间复制,年龄递增。
- 晋升条件:
- 对象年龄达到阈值(默认 15 次)。
- 动态年龄判定:相同年龄对象总和超过 Survivor 区一半,大于等于该年龄的对象直接晋升。
- 大对象(超过
PretenureSizeThreshold
)直接进入老年代。
5. 什么是 Minor GC、Major GC 和 Full GC?
- Minor GC(新生代 GC):清理新生代,触发频繁,速度快。
- Major GC(老年代 GC):清理老年代,通常伴随至少一次 Minor GC。
- Full GC(整堆 GC):清理整个堆(新生代、老年代、元空间),触发条件包括:
- 老年代空间不足。
- 元空间满。
- 显式调用
System.gc()
。 - CMS GC 时并发模式失败。
6. 常见的垃圾回收器有哪些?简述其特点。
- 新生代回收器:
- Serial:单线程,适合客户端应用。
- ParNew:Serial 的多线程版本,与 CMS 配合。
- Parallel Scavenge:多线程,注重吞吐量。
- 老年代回收器:
- Serial Old:单线程,用于 Client 模式或与 CMS 配合。
- Parallel Old:多线程,与 Parallel Scavenge 配合。
- CMS:以低停顿为目标,并发标记和清除,会产生碎片。
- G1:分代收集,将堆划分为 Region,优先回收垃圾最多的区域。
- 全区域回收器:
- ZGC:超低延迟,支持 TB 级内存。
7. CMS 和 G1 垃圾回收器的区别是什么?
特性 | CMS | G1 |
---|---|---|
回收区域 | 仅针对老年代 | 全堆(分 Region) |
算法 | 标记 - 清除(会产生碎片) | 标记 - 整理(减少碎片) |
停顿时间 | 低停顿,并发标记和清除 | 可预测的停顿,优先回收价值高的 Region |
适用场景 | 响应时间敏感的 Web 应用 | 大内存、多 CPU 的服务器应用 |
内存布局 | 连续的老年代空间 | 划分为多个大小相等的 Region |
8. 什么是内存泄漏?如何排查?
内存泄漏:对象不再使用,但 GC 无法回收其占用的内存(如静态集合持有对象引用、未关闭的资源等)。
排查方法:
- 工具:使用
jstat
、jmap
生成堆转储文件,通过MAT
或jhat
分析。 - 步骤:
- 监控内存使用趋势(持续增长可能存在泄漏)。
- 对比不同时间点的堆转储文件,找出增长异常的类。
- 检查对象引用链,确定泄漏原因(如静态集合、单例持有对象)。
9. 如何优化 Java GC 性能?
- 选择合适的回收器:
- Web 应用:CMS/G1(低延迟)。
- 科学计算:Parallel(高吞吐量)。
- 调整堆大小:
-Xms
/-Xmx
设置相同值,避免动态扩展。- 合理分配新生代和老年代比例(如新生代占 1/3 堆内存)。
- 减少 Full GC:
- 避免大对象直接进入老年代。
- 控制对象晋升阈值,减少老年代压力。
- 监控与分析:
- 启用 GC 日志,使用工具(如 GCeasy)分析停顿时间和频率。
10. 什么是 OOM(OutOfMemoryError)?常见原因有哪些?
OOM:JVM 无法分配新内存且 GC 无法回收足够空间时抛出的错误。
常见原因:
- 堆内存不足(
-Xmx
设置过小或内存泄漏)。 - 元空间 / 永久代满(动态加载大量类,如框架反射)。
- 线程创建过多(每个线程占用栈空间)。
- 直接内存溢出(如 NIO 使用
ByteBuffer.allocateDirect()
)。
11. 简述 GC Roots 的类型。
GC Roots 是可达性分析的起始点,包括:
- 虚拟机栈(栈帧中的本地变量表):方法执行时创建的局部变量。
- 方法区中的类静态属性引用的对象:如
static Object obj
。 - 方法区中的常量引用的对象:如字符串常量池中的引用。
- 本地方法栈中 JNI(Native 方法)引用的对象。
12. 如何理解 “Stop The World”?
Stop The World(STW):GC 执行时,JVM 暂停所有用户线程的现象。
- 原因:为确保对象引用关系在分析期间不变,需暂停用户线程。
- 影响:高并发场景下可能导致系统响应卡顿。
- 优化:使用并发回收器(如 CMS、G1、ZGC)减少 STW 时间。
13. 简述 G1 回收器的工作流程。
G1 将堆划分为多个大小相等的 Region,工作流程:
- 初始标记(Initial Mark):STW,标记 GC Roots 直接关联的对象。
- 并发标记(Concurrent Marking):与用户线程并发执行,遍历对象图。
- 最终标记(Final Mark):STW,修正并发标记期间的变动。
- 筛选回收(Live Data Counting and Evacuation):
- 计算各 Region 的回收价值,优先回收垃圾最多的 Region。
- 使用复制算法,将存活对象移至新 Region,减少碎片。
14. -XX:+UseConcMarkSweepGC
和-XX:+UseG1GC
的区别是什么?
- -XX:+UseConcMarkSweepGC:启用 CMS 回收器,适用于老年代,以低停顿为目标,采用并发标记 - 清除算法,但会产生内存碎片,可能触发 Full GC。
- -XX:+UseG1GC:启用 G1 回收器,全堆回收,将堆划分为 Region,优先回收价值高的区域,可预测停顿时间,适合大内存应用。
15. 如何监控 Java GC?
- 命令行工具:
jstat -gc <pid> <interval>
:查看 GC 统计信息。jmap -heap <pid>
:查看堆内存使用情况。jcmd <pid> GC.heap_dump
:生成堆转储文件。
- 可视化工具:
- VisualVM、Java Mission Control(JMC)、GCeasy(分析 GC 日志)。
- GC 日志:
- 添加 JVM 参数
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps
记录 GC 信息。
- 添加 JVM 参数
总结
理解 GC 机制的核心原理(分代收集、可达性分析、回收算法)、常见回收器的特性,以及如何通过工具监控和优化 GC 性能,是面试中的高频考点。建议结合实际项目经验,深入理解 GC 调优的思路和方法。