JVM 的 GC(Garbage Collection,垃圾收集) 是 Java 自动内存管理的核心机制。它的主要任务是自动识别并回收不再使用的对象,释放内存空间,防止内存泄漏和溢出。
🧠 一、GC 的基本原理
1. 什么是“垃圾”?
在 JVM 中,“垃圾”指的是 已经不再被任何引用链访问到的对象,也就是程序中无法再访问到的“无用对象”。
2. 如何判断对象是否可回收?
JVM 使用 可达性分析算法(Reachability Analysis):
- 从一组称为 GC Roots 的对象开始,向下搜索引用链。
- 如果一个对象与 GC Roots 之间 没有引用路径相连,则该对象被认为是“不可达”的,可以被回收。
✅ 常见的 GC Roots 包括:
- 虚拟机栈(栈帧中的本地变量表)中的引用对象
- 方法区中的类静态属性引用的对象
- 方法区中的常量引用的对象
- 本地方法栈中 JNI(Native 方法)的引用对象
- 正在被运行的线程对象
🎯 举例:
一个对象A被局部变量引用 → 局部变量在栈中 → 栈是 GC Roots → 所以A可达,不会被回收。
如果局部变量出作用域,引用消失 →A不可达 → 可被回收。
🧹 二、GC 是如何清理内存的?(详细过程)
GC 的清理过程分为以下几个阶段,不同垃圾收集器实现略有差异,但基本流程相似。
阶段 1:标记(Mark)
- 从 GC Roots 出发,遍历所有可达对象,给它们打上“存活”标记。
- 所有没有被标记的对象,就是“垃圾”。
⚠️ 问题:如果对象很多,标记过程会暂停应用线程(Stop-The-World),影响性能。
阶段 2:清除(Sweep)
根据不同的 GC 算法,有多种清除方式:
🔹 2.1 标记-清除(Mark-Sweep)
- 直接回收未被标记的对象内存。
- 缺点:
- 产生内存碎片(不连续的空闲内存)
- 分配大对象时可能失败,即使总内存足够
🔹 2.2 标记-整理(Mark-Compact)
- 将所有存活对象向内存一端移动,然后清理边界以外的内存。
- 优点:消除内存碎片
- 缺点:移动对象需要时间,暂停时间更长
🔹 2.3 复制算法(Copying)
- 将内存分为两块(如 Eden + Survivor),每次只使用一块。
- GC 时,将存活对象复制到另一块,然后清空原区域。
- 优点:无碎片,效率高
- 缺点:内存利用率只有 50%
💡 现代 JVM 通常结合多种算法,例如新生代用 复制算法,老年代用 标记-整理。
🔄 三、JVM 的分代收集模型(Generational Collection)
JVM 根据对象的生命周期将堆内存分为 新生代(Young) 和 老年代(Old),采用不同的 GC 策略。
1. 新生代(Young Generation)
- 存放新创建的对象
- 大多数对象“朝生夕死”,很快变成垃圾
- 使用 复制算法,GC 频繁但速度快
新生代结构:
Eden + Survivor0 + Survivor1
- 对象先分配在 Eden
- Minor GC 后,存活对象复制到 Survivor
- 经过多次 GC 仍存活的对象,晋升到老年代
📌 通常设置
-XX:MaxTenuringThreshold控制晋升年龄。
2. 老年代(Old Generation)
- 存放长期存活的对象
- GC 频率低,但耗时长
- 使用 标记-整理 或 标记-清除 算法
🛠️ 四、常见的垃圾收集器(Garbage Collectors)
| 收集器 | 适用区域 | 算法 | 特点 |
|---|---|---|---|
| Serial | 新生代 | 复制 | 单线程,简单高效,适用于客户端 |
| Parallel(吞吐量优先) | 新生代/老年代 | 复制 / 标记-整理 | 多线程,关注吞吐量 |
| CMS(Concurrent Mark Sweep) | 老年代 | 标记-清除 | 并发收集,低停顿,但有碎片 |
| G1(Garbage First) | 整个堆 | 标记-整理 + 复制 | 面向大内存,可预测停顿时间 |
| ZGC / Shenandoah | 整个堆 | 读屏障 + 并发整理 | 超低延迟,支持 TB 级堆 |
🔍 G1 收集器工作流程(以 G1 为例):
G1 将堆划分为多个 Region,优先回收垃圾最多的 Region。
- 初始标记(Initial Mark):标记 GC Roots 直接引用的对象(STW)
- 并发标记(Concurrent Mark):并发遍历对象图
- 最终标记(Remark):处理并发阶段的变动(STW)
- 筛选回收(Copy/Compact):选择回收价值最高的 Region 进行清理
✅ G1 的优势:可设置最大停顿时间(
-XX:MaxGCPauseMillis)
⚙️ 五、GC 的触发时机
| GC 类型 | 触发条件 |
|---|---|
| Minor GC | Eden 区满时触发 |
| Major GC / Full GC | 老年代空间不足、方法区满、调用 System.gc() 等 |
❗ Full GC 会导致整个应用暂停(Stop-The-World),应尽量避免频繁发生。
📊 六、GC 的“Stop-The-World”(STW)
- 在某些 GC 阶段(如标记、整理),JVM 必须暂停所有应用线程,确保对象图一致性。
- STW 时间越长,应用响应越差。
- 现代 GC(如 G1、ZGC)通过并发标记、增量整理等方式 减少 STW 时间。
🔍 七、如何监控和分析 GC?
1. 开启 GC 日志
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log
2. 常用命令
jstat -gc <pid> # 查看 GC 统计
jmap -heap <pid> # 查看堆内存使用
jconsole # 图形化监控
jvisualvm # 强大的分析工具
3. 分析工具
- GCViewer:分析 GC 日志,查看吞吐量、停顿时间
- Eclipse MAT:分析堆转储,查找内存泄漏
- JProfiler:商业级性能分析工具
🚨 常见 GC 问题与调优建议
| 问题 | 可能原因 | 解决方案 |
|---|---|---|
| 频繁 Minor GC | Eden 区太小 | 增大 -Xmn |
| 老年代增长快 | 对象过早晋升 | 调整晋升年龄或增大 Survivor |
| Full GC 频繁 | 老年代空间不足 | 增大 -Xmx,优化对象生命周期 |
| GC 停顿时间长 | 使用 Serial 或 CMS | 改用 G1 或 ZGC |
OutOfMemoryError | 内存泄漏或配置不足 | 分析堆转储,修复泄漏,调整参数 |
✅ 总结:GC 是如何工作的?
- 判断垃圾:通过可达性分析,找出不可达对象
- 标记存活对象:从 GC Roots 开始遍历
- 清理内存:根据算法(复制、标记-清除、标记-整理)回收垃圾
- 分代收集:新生代用复制算法,老年代用标记-整理
- 选择收集器:根据应用需求选择合适的 GC(如 G1 适合大堆低延迟)
- 监控调优:通过日志和工具分析 GC 行为,优化参数
💡 一句话总结:
GC 就像一个自动清洁工,定期检查哪些对象“没人用了”,然后把它们清理掉,腾出空间给新对象使用。
如果你有具体的 GC 日志、OOM 错误,或者想了解如何为你的应用选择合适的 GC 策略,欢迎提供更多信息,我可以给出针对性的优化建议!
3074

被折叠的 条评论
为什么被折叠?



