1. 为什么需要了解垃圾收集器
作为一名Java开发者,你可能经常听到"JVM调优"这个词,而垃圾收集器(GC)正是调优的核心环节。想象一下,你的Java应用就像一座不断产生垃圾的城市,而垃圾收集器就是城市的清洁工系统。如果清洁工效率低下,城市就会堆满垃圾(内存泄漏);如果清洁工太勤快,又会占用太多资源(GC开销)。这就是为什么我们需要深入了解不同类型的清洁工(垃圾收集器)及其工作方式。
在实际项目中,我曾遇到一个电商系统在促销期间频繁卡顿的问题。通过JVM监控发现,默认的垃圾收集器在高并发场景下出现了长时间的"Stop The World"(全线暂停),导致用户下单延迟。更换为G1收集器后,系统吞吐量提升了40%。这个案例让我深刻认识到,选择适合业务场景的收集器多么重要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JVM内存模型与GC的关系
2.1 JVM内存区域划分
要理解垃圾收集器,首先需要了解JVM的内存结构。JVM内存主要分为以下几个区域:
- 堆(Heap):对象实例的存储区域,也是GC的主要工作场所
- 方法区(Method Area):存储类信息、常量、静态变量等
- 虚拟机栈(VM Stack):线程私有的方法调用栈
- 本地方法栈(Native Method Stack):本地方法调用
- 程序计数器(PC Register):线程执行位置记录
其中,堆区又细分为:
- 新生代(Young Generation):新创建对象的存放区域
- Eden区
- Survivor区(通常有两个:From和To)
- 老年代(Old Generation):长期存活对象的存放区域
2.2 对象生命周期与GC触发条件
一个新对象通常的"人生轨迹"是这样的:
- 出生在Eden区
- 经历Minor GC后存活,被移动到Survivor区
- 在Survivor区中"熬过"一定次数的GC(默认15次),晋升到老年代
- 最终在老年代中被Major GC回收
GC的触发条件主要有:
- Eden区空间不足时触发Minor GC
- 老年代空间不足时触发Major GC
- 方法区空间不足时触发Full GC
- 调用System.gc()建议执行GC(但不保证立即执行)
3. 主流垃圾收集器详解
3.1 Serial收集器
Serial收集器是最古老、最简单的收集器,它的特点是:
- 单线程工作
- 进行垃圾回收时会暂停所有应用线程(Stop The World)
- 采用复制算法处理新生代
- 采用标记-整理算法处理老年代
适用场景:
- 客户端模式下的JVM
- 内存资源受限的嵌入式系统
- 单核处理器环境
配置参数:
code复制-XX:+UseSerialGC
3.2 Parallel收集器
Parallel收集器(也称吞吐量收集器)是Serial的多线程版本:
- 多线程并行执行垃圾回收
- 仍然会暂停所有应用线程
- 目标是最大化吞吐量(吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间))
适用场景:
- 多核处理器环境
- 注重吞吐量的后台批处理系统
- 对延迟不敏感的应用
配置参数:
code复制-XX:+UseParallelGC
-XX:+UseParallelOldGC
-XX:ParallelGCThreads=<N> # 设置GC线程数
3.3 CMS收集器
CMS(Concurrent Mark-Sweep)收集器是第一款真正意义上的并发收集器:
- 大部分工作与用户线程并发执行
- 采用标记-清除算法
- 目标是减少停顿时间
工作流程分为四个阶段:
- 初始标记(短暂STW)
- 并发标记
- 重新标记(短暂STW)
- 并发清除
缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾
- 会产生内存碎片
适用场景:
- 重视响应速度的Web应用
- 老年代较大的系统
- 有足够CPU资源支持并发
配置参数:
code复制-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=<N> # 老年代使用率触发阈值
3.4 G1收集器
G1(Garbage-First)收集器是JDK9及以后的默认收集器:
- 将堆划分为多个大小相等的Region
- 优先回收垃圾最多的Region(Garbage-First)
- 可预测的停顿时间模型
- 同时处理新生代和老年代
工作流程:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
优势:
- 内存碎片问题大大减少
- 可设置最大停顿时间目标
- 对大堆处理更高效
配置参数:
code复制-XX:+UseG1GC
-XX:MaxGCPauseMillis=<N> # 目标最大停顿时间
-XX:G1HeapRegionSize=<N> # Region大小
4. 收集器选择与调优实践
4.1 如何选择合适的收集器
选择收集器时需要考虑以下因素:
- 应用特性:Web服务?批处理?实时系统?
- 硬件资源:CPU核心数?内存大小?
- 性能目标:吞吐量优先?低延迟优先?
一般建议:
- 小型应用:Serial
- 吞吐量优先:Parallel
- 低延迟优先:CMS或G1
- 大堆内存:G1或ZGC
4.2 常见调优参数
通用参数:
code复制-Xms -Xmx # 堆初始和最大大小
-XX:NewRatio # 新生代与老年代比例
-XX:SurvivorRatio # Eden与Survivor区比例
针对G1的特别参数:
code复制-XX:InitiatingHeapOccupancyPercent # 触发并发GC周期的堆占用率
-XX:G1ReservePercent # 保留内存比例
4.3 监控与诊断工具
-
命令行工具:
- jstat:查看GC统计信息
- jmap:堆内存分析
- jstack:线程堆栈分析
-
可视化工具:
- VisualVM
- JConsole
- GC日志分析工具(如GCViewer)
-
GC日志配置:
code复制-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:<file-path>
5. 常见问题与解决方案
5.1 GC overhead limit exceeded
这是JVM抛出的一种OutOfMemoryError,表示GC花费了太多时间(超过98%)却回收了很少内存(少于2%)。
解决方案:
- 增加堆大小
- 优化对象创建模式
- 调整GC策略
5.2 Concurrent Mode Failure
CMS收集器在并发阶段老年代空间不足时会发生此错误。
解决方案:
- 降低CMS触发阈值(-XX:CMSInitiatingOccupancyFraction)
- 增加老年代大小
- 提前触发CMS收集
5.3 Promotion Failed
对象从新生代晋升到老年代时失败,通常因为老年代空间不足。
解决方案:
- 增加Survivor区大小
- 降低晋升阈值(-XX:MaxTenuringThreshold)
- 增加老年代空间
6. 新一代收集器展望
虽然G1已经是相当成熟的收集器,但JVM生态仍在不断发展。值得关注的新一代收集器包括:
-
ZGC:
- 目标:亚毫秒级停顿
- 特点:并发压缩、着色指针、内存多重映射
- 适用:超大堆(TB级别)
-
Shenandoah:
- 与ZGC类似的目标
- 不同实现方式:Brooks指针
- 更早的版本支持
这些收集器在特定场景下可以带来显著的性能提升,但同时也需要相应的硬件支持和JDK版本。
