3.垃圾回收

#jvm #gc #垃圾回收

1. 什么时候触发 GC?

年轻代内存不够时触发 Young GC,老年代内存不够时触发 Old GC,两者都回收叫 Full GC。

可达性分析判断对象是否存活:从 GC Roots(局部变量、静态变量、常量)出发,能被引用链到达的存活,到达不了的是垃圾。内存区域划分见 2.JVM内存划分

循环引用不会影响可达性分析。 A 引用 B、B 引用 A,但两者都没有被 GC Roots 引用,从 GC Roots 出发根本到不了它们,照样会被回收。

早期的引用计数法才会被循环引用坑到——A 和 B 互相引用,引用计数都是 1,永远不会降到 0,就算没人用它们也回收不掉。Java 没有用引用计数法,所以这个问题不存在。

Java 里真正的内存泄漏不是循环引用,而是对象一直被 GC Roots 引用链持有,但业务上已经不需要了:

2. 回收算法

复制算法(年轻代):把存活对象复制到另一块区域,原区域整体清空。Eden + 两个 Survivor 就是复制算法的实现,只有 10% 内存闲置。

标记整理算法(老年代):标记存活对象,把它们移到一端,清理边界外的内存。没有内存碎片,但移动对象开销大。

3. 何时进入老年代?

大对象直接进老年代:`-XX:PretenureSizeThreshold=1m

Young GC 后存活对象超过 Survivor 容量,直接溢出到老年代

# 年轻代 1G
-XX:SurvivorRatio=8 # Eden:S0:S1 = 8:1:1,Survivor 各 100M
-XX:MaxTenuringThreshold=15 # 最大年龄阈值
-XX:TargetSurvivorRatio=50

4. 垃圾回收器察晋升情况


加上 `-XX:+PrintTenuringDistribution` 后,GC 日志会输出类似:

Desired survivor size 52428800 bytes, new threshold 6 (max 15)


这里 age 3 累计超过了 50M 阈值,JVM 把 `new threshold` 调整为 3,下次 GC 后年龄 ≥ 3 的对象全部晋升老年代,不用等到 15 次

### 4.1 ParNew + CMS(JDK 8 常用)

ParNew 回收年轻代(多线程),CMS 回收老年代(并发,低停顿)。

CMS 四个阶段:
1. 初始标记(STW,很快)
2. 并发标记(耗时,不停顿业务)
3. 重新标记(STW,很快)
4. 并发清理(耗时,不停顿业务)

CMS 的问题:
- 并发标记占用 CPU,默认线程数 `(CPU核数+3)/4`
- 产生内存碎片,需要定期整理:`-XX:+UseCMSCompactAtFullCollection`
- `Concurrent Mode Failure`:老年代剩余空间不足以容纳并发期间新晋升的对象,触发 Serial Old 单线程 Full GC,停顿时间很长

CMS 常用配置:

```bash
-XX:+UseParNewGC -XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=92
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSParallelInitialMarkEnabled
-XX:+CMSScavengeBeforeRemark

4.2 G1(JDK 9+ 推荐)

把堆拆成多个等大的 Region,逻辑上区分年轻代和老年代,但不再是物理上连续的区域。可以设置 GC 停顿时间目标,6.G1垃圾回收器 根据每个 Region 的回收价值动态选择回收哪些。

关键参数:

-XX:+UseG1GC
-XX:MaxGCPauseMillis=200        # 停顿时间目标,默认 200ms
-XX:G1HeapRegionSize=2m         # Region 大小,堆/2048
-XX:InitiatingHeapOccupancyPercent=45  # 老年代占比触发 Mixed GC 的阈值

G1 的优化核心是控制停顿时间,给足内存,合理设置 MaxGCPauseMillis。如果 G1 还是满足不了停顿要求,可以考虑升级到 5.ZGC学习

5. OOM 常见场景

OOM 类型 常见原因
堆内存 OOM 内存泄漏、高并发对象太多
MetaSpace OOM 动态生成类过多(cglib、反射)、MetaSpace 设置太小
虚拟机栈 OOM 无限递归调用
堆外内存 OOM DirectBuffer 未释放,见 JAVA的堆外内存,回收机制见 Java虚引用