写 Java 代码时,你可能写过这样的代码:
while(true){List<byte[]>list=newArrayList<>();for(inti=0;i<1000;i++){list.add(newbyte[1024*1024]);// 1MB}Thread.sleep(1000);}运行一段时间后,程序可能会卡顿,甚至OOM。这背后就是垃圾回收(GC)在起作用。
理解 GC,能帮你:
- 排查线上 OOM 问题
- 优化程序性能,减少 GC 停顿
- 合理配置 JVM 参数
- 选型合适的垃圾收集器
下面我按「分代模型 → GC 算法 → 垃圾收集器 → GC 日志」的顺序往下聊。
1. 为什么需要 GC?🧹
1.1 内存泄漏与 OOM
Java 程序运行时会不断创建对象,这些对象存在堆内存中。如果对象不再使用但没有被回收,堆内存会逐渐耗尽,最终导致OutOfMemoryError。
// 内存泄漏示例List<byte[]>list=newArrayList<>();while(true){list.add(newbyte[1024*1024]);// 不断添加,不清理Thread.sleep(1000);}1.2 GC 的作用
GC(Garbage Collection)自动回收不再使用的对象,释放内存空间。
// GC 自动回收List<byte[]>list=newArrayList<>();for(inti=0;i<1000;i++){list.add(newbyte[1024*1024]);}// 循环结束后,list 超出作用域,变成垃圾// GC 会自动回收这些 byte[] 对象2. JVM 分代模型 🧬
2.1 堆内存分代
JVM 堆内存分为新生代和老年代:
堆内存(Heap): │ ├─ 新生代(Young Generation) │ ├─ Eden 区:新对象分配 │ ├─ Survivor 区 S0(From) │ └─ Survivor 区 S1(To) │ └─ 老年代(Old Generation) └─ 长期存活的对象2.2 新生代
新生代存放新创建的对象,大多数对象生命周期很短。
- Eden 区:新对象分配区
- Survivor 区(S0、S1):存放经历一次 Minor GC 后仍然存活的对象
// 大多数对象在新生代分配Useruser=newUser();// 在 Eden 区2.3 老年代
老年代存放长期存活的对象:
- 经历多次 Minor GC 后仍然存活的对象
- 大对象(直接分配)
// 大对象直接进入老年代byte[]large=newbyte[10*1024*1024];// 10MB2.4 元空间(方法区)
元空间(Java 8+)存放类信息、常量、静态变量:
Java 7 及之前:永久代(PermGen) Java 8 及之后:元空间(Metaspace)3. GC 类型与触发条件 🔄
3.1 Minor GC(Young GC)
Minor GC发生在新生代,清理 Eden 区和 Survivor 区。
触发条件:Eden 区空间不足
特点:
- 频率高
- 停顿时间短
- 采用复制算法
3.2 Major GC / Full GC
Major GC发生在老年代,清理老年代。
Full GC清理整个堆(新生代 + 老年代)。
触发条件:
- 老年代空间不足
- 调用
System.gc() - 元空间不足
- Minor GC 后晋升对象大小 > Survivor 区
特点:
- 频率低
- 停顿时间长
- 影响系统性能
3.3 GC 日志示例
[GC (Allocation Failure) [PSYoungGen: 512K->64K(1536K)] 1024K->512K(2048K), 0.0012345 secs]4. GC 算法 📊
4.1 标记-清除算法(Mark-Sweep)
步骤:
- 标记所有需要回收的对象
- 清除标记的对象
缺点:
- 产生内存碎片
- 效率不高
标记-清除: 标记前:□ ■ □ ■ ■ □ 标记后:□ ■ ■ ■ ■ □ (■ 是垃圾) 清除后:□ □ □ □ □ (碎片)4.2 复制算法(Copying)
将内存分为两块,每次只使用一块,回收时将存活对象复制到另一块。
优点:无碎片
缺点:内存利用率低
复制算法: ┌─────────┬─────────┐ │ 使用中 │ 空闲 │ └─────────┴─────────┘ 回收时:把存活对象复制到空闲区4.3 标记-整理算法(Mark-Compact)
步骤:
- 标记需要回收的对象
- 整理存活对象,向一端移动
- 清除边界外的对象
优点:无碎片
缺点:效率低
标记-整理: 标记前:□ ■ □ ■ ■ □ 整理后:□ □ □ □ □ ■ (存活对象移动到一端) 清除后:□ □ □ □ □5. 垃圾收集器 🚀
5.1 收集器分类
| 收集器 | 作用区域 | 算法 | 特点 |
|---|---|---|---|
| Serial | 新生代 | 复制 | 单线程,停顿长 |
| ParNew | 新生代 | 复制 | 多线程版本 |
| Parallel Scavenge | 新生代 | 复制 | 吞吐量优先 |
| Serial Old | 老年代 | 标记-整理 | 单线程 |
| Parallel Old | 老年代 | 标记-整理 | 多线程 |
| CMS | 老年代 | 标记-清除 | 并发收集,低停顿 |
| G1 | 全堆 | 标记-整理 | 分区域,可预测停顿 |
| ZGC | 全堆 | 标记-整理 | 并发,极低停顿 |
5.2 Serial 收集器
最古老的收集器,单线程执行。
# 启用 Serial 收集器-XX:+UseSerialGC特点:
- 单线程
- 简单高效
- 停顿时间长
适用场景:客户端模式、内存较小的应用
5.3 Parallel 收集器
吞吐量优先的收集器,多线程并行收集。
# 启用 Parallel 收集器-XX:+UseParallelGC# 设置线程数-XX:ParallelGCThreads=4特点:
- 多线程
- 吞吐量高
- 适合后台应用
适用场景:批处理、科学计算
5.4 CMS 收集器
并发收集器,低停顿。
# 启用 CMS 收集器-XX:+UseConcMarkSweepGC工作阶段:
- 初始标记(STW):标记 GC Roots
- 并发标记:并发追踪存活对象
- 重新标记(STW):修正标记
- 并发清除:清除垃圾
特点:
- 并发收集
- 停顿短
- 产生内存碎片
适用场景:互联网应用
5.5 G1 收集器
面向服务端的收集器,可预测停顿。
# 启用 G1 收集器-XX:+UseG1GC# 设置停顿目标-XX:MaxGCPauseMillis=200特点:
- 分区域(Region)
- 可预测停顿
- 整理碎片
- 并发收集
适用场景:大内存应用
5.6 ZGC 收集器
超低停顿的收集器。
# 启用 ZGC-XX:+UseZGC特点:
- 并发收集
- 停顿 < 10ms
- 支持 TB 级内存
适用场景:大内存、低停顿应用
5.7 收集器组合
# 组合 1:Serial + Serial Old-XX:+UseSerialGC# 组合 2:ParNew + CMS-XX:+UseParNewGC-XX:+UseConcMarkSweepGC# 组合 3:Parallel + Parallel Old-XX:+UseParallelGC# 组合 4:G1(独立使用)-XX:+UseG1GC6. GC 日志解读 📝
6.1 开启 GC 日志
# 打印 GC 日志-XX:+PrintGCDetails-XX:+PrintGCDateStamps-Xloggc:gc.log# 示例java-XX:+PrintGCDetails-Xloggc:gc.logMyApp6.2 Minor GC 日志
[GC (Allocation Failure) [PSYoungGen: 512K->64K(1536K)] // 新生代:回收前->回收后(总大小) 1024K->512K(2048K), // 堆:回收前->回收后(总大小) 0.0012345 secs] // 停顿时间6.3 Full GC 日志
[Full GC (Allocation Failure) [PSYoungGen: 512K->0K(1536K)] [ParOldGen: 1024K->1023K(2048K)] 1536K->1023K(3584K), 0.0123456 secs]6.4 CMS 日志
[GC [1 CMS-initial-mark: 1024K(2048K)] 1024K(2048K), 0.0012345 secs] [GC [CMS-concurrent-mark-start] [GC [CMS-concurrent-mark: 0.0123456/0.0123456 secs] [GC [CMS-concurrent-sweep-start] [GC [CMS-concurrent-sweep: 0.0234567/0.0234567 secs] [GC [CMS-concurrent-reset-start] [GC [CMS-concurrent-reset: 0.0001234/0.0001234 secs]6.5 G1 日志
[GC pause (G1 Evacuation Pause) young 512M->128M(2048M), 0.0123456 secs] [GC concurrent-root-region-scan-start] [GC concurrent-root-region-scan-end, 0.0012345 secs] [GC concurrent-mark-start] [GC concurrent-mark-end, 0.0234567 secs]7. GC 调优实战 🎯
7.1 常见 GC 问题
| 问题 | 现象 | 原因 |
|---|---|---|
| Minor GC 频繁 | 频繁 minor gc | 对象分配太快 |
| Full GC 频繁 | 频繁 full gc | 内存不足、大对象 |
| GC 停顿长 | 页面卡顿 | 堆太大、GC 选型不当 |
| OOM | 程序崩溃 | 内存泄漏、堆太小 |
7.2 调优思路
# 1. 调整堆大小-Xms2g-Xmx2g# 2. 调整新生代比例-Xmn512m# 新生代 512MB-XX:NewRatio=2# 新生代:老年代 = 1:2# 3. 调整 Survivor 比例-XX:SurvivorRatio=8# Eden:Survivor = 8:1# 4. 选择收集器-XX:+UseG1GC-XX:MaxGCPauseMillis=2007.3 监控工具
# jstat:查看 GC 统计jstat-gcutil<pid>1000# jmap:查看堆内存jmap-heap<pid># jcmd:综合诊断jcmd<pid>GC.heap_info小结
- GC自动回收不再使用的对象,释放内存
- 分代模型:新生代(Eden + Survivor)、老年代、元空间
- GC 算法:标记-清除、复制、标记-整理
- 垃圾收集器:Serial、Parallel、CMS、G1、ZGC
- CMS:并发收集,低停顿,适合互联网应用
- G1:分区域收集,可预测停顿,适合大内存应用
- GC 日志可以反映 GC 频率、停顿时间、内存使用情况
下一篇(024)预告:JVM 参数入门:堆、栈、元空间与典型模板——常用 JVM 参数、GC 调优参数、性能优化模板。