3. GC 基础与停顿原理:JVM 堆内存结构与 STW 成因
在数据库迁移后的故障排查中,连接池泄漏往往与 GC 停顿相互叠加,形成恶性循环。理解 JVM 堆内存结构、GC 算法以及 Stop-The-World(STW)停顿的成因,是定位这类问题的理论基础。本章将系统梳理这些核心概念,并揭示它们与连接池泄漏之间的关联。
3.1 JVM 堆内存结构
JVM 堆是 GC 管理的主要区域,按照对象的存活周期划分为不同代(Generation),以便采用差异化的回收策略。以下是 JDK 8+ 的典型堆结构:
| 区域 | 默认占比(堆大小) | 主要存储内容 | GC 行为 |
|---|---|---|---|
| 年轻代(Young Gen) | 1/3 堆 | 新创建的对象 | Minor GC 频繁,采用复制算法 |
| — Eden 区 | 年轻代的 8/10 | 对象首次分配的区域 | Minor GC 后存活对象移至 Survivor |
| — Survivor 区(S0/S1) | 年轻代的 1/10 各 | 经历一次 GC 仍存活的对象 | 交替作为 To/From 空间 |
| 老年代(Old Gen) | 2/3 堆 | 长期存活的对象(年龄阈值默认 15) | Major GC / Full GC,采用标记-整理或标记-清除 |
| 元空间(Metaspace) | 本地内存(非堆) | 类元数据、方法信息、常量池 | 触发条件为元空间大小达到 MaxMetaspaceSize |
关键点: 连接池泄漏时,大量空闲连接对象无法被回收,会从年轻代快速晋升到老年代,导致老年代占用持续增长,最终触发 Full GC。
3.2 GC 算法与核心原理
不同的 GC 算法在吞吐量、停顿时间和内存碎片之间做出权衡。以下是三种基础算法的对比:
| 算法 | 核心步骤 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 标记-清除 | 1. 标记存活对象 2. 清除未标记对象 |
实现简单,不移动对象 | 产生内存碎片,分配效率下降 | 老年代(CMS 收集器) |
| 复制 | 1. 将存活对象复制到另一块空间 2. 清空原空间 |
无碎片,分配高效(bump-the-pointer) | 内存利用率低(保留一半空闲) | 年轻代(Minor GC) |
| 标记-整理 | 1. 标记存活对象 2. 将所有存活对象向一端移动 |
无碎片,内存利用率高 | 移动对象开销大,STW 时间长 | 老年代(Parallel Old / G1) |
连接池泄漏场景下的 GC 特征: 泄漏的连接对象在年轻代无法被回收,经过多次 Minor GC 后晋升到老年代。老年代空间被大量“假存活”对象占据,导致 Full GC 频繁触发,且标记-整理阶段需要移动大量对象,STW 停顿时间显著延长。
3.3 STW(Stop-The-World)停顿的成因与影响
STW 是指 GC 执行期间,所有应用线程被暂停,只有 GC 线程运行。停顿的时长直接影响系统的响应延迟和吞吐量。
3.3.1 STW 的成因
- 安全点(Safepoint)机制: JVM 要求所有线程在 GC 前必须到达一个安全点,此时线程状态已知,可以安全地修改对象引用。
- 根对象枚举: GC 需要从 GC Roots(栈帧、静态变量、JNI 引用等)出发,遍历所有存活对象。此过程必须在线程暂停时完成,防止引用关系变化。
- 对象移动与引用更新: 复制算法和标记-整理算法需要移动对象,并更新所有指向被移动对象的引用。这要求所有线程暂停,避免并发访问导致数据不一致。
3.3.2 STW 停顿的影响
| 影响维度 | 具体表现 | 与连接池泄漏的关联 |
|---|---|---|
| 响应延迟 | 请求处理时间增加,出现毛刺(Spike) | 泄漏导致 Full GC 频繁,STW 时间从几十毫秒飙升到数秒 |
| 吞吐量下降 | 应用线程执行时间占比降低 | GC 线程占用 CPU,应用线程被暂停,TPS 下降 |
| 连接池耗尽 | STW 期间应用无法归还连接,连接池被占满 | 泄漏 + STW 导致连接无法释放,新请求获取连接超时 |
| 系统雪崩 | 数据库连接超时、上游服务重试、负载飙升 | STW 停顿引发连锁故障,最终导致系统不可用 |
典型故障模式: 连接池泄漏 → 老年代占用持续增长 → Full GC 触发 → STW 停顿 2-5 秒 → 应用线程暂停 → 连接无法归还 → 连接池耗尽 → 新请求获取连接失败 → 上游服务重试 → 负载进一步升高 → 系统雪崩。这是数据库迁移后最常见的故障链路之一。
3.4 连接池泄漏与 GC 停顿的恶性循环
连接池泄漏和 GC 停顿并非孤立问题,它们会相互加剧:
- 泄漏导致 GC 压力增大: 泄漏的连接对象无法被回收,长期驻留在老年代,迫使 JVM 更频繁地触发 Full GC。
- GC 停顿加剧泄漏: STW 期间,应用线程无法执行连接归还逻辑,导致连接池中的连接被长时间占用,进一步加剧泄漏。
- 恶性循环形成: 泄漏 → 老年代满 → Full GC(STW 长) → 连接无法归还 → 更多连接泄漏 → 老年代更满 → 更频繁的 Full GC。
排查切入点: 在故障发生时,通过
jstat -gcutil 观察老年代占用率(OU)和 Full GC 次数(FGC),结合 jstack 查看线程状态,可以快速判断是否处于泄漏 + GC 停顿的恶性循环中。