3. JVM 内存模型与 GC 基础:堆内存结构、新生代与老年代、GC 算法简介
在深入 Flyway 迁移脚本的 GC 调优之前,必须建立 JVM 堆内存与垃圾回收(GC)的底层认知。Flyway 脚本执行慢的根因,往往不是 SQL 本身,而是 GC 停顿(Stop-The-World, STW)与堆内存结构不匹配导致的。本节将拆解堆内存分区、对象晋升路径以及三大核心 GC 算法,为后续的停顿分析打下基础。
3.1 堆内存结构:分代设计的核心逻辑
JVM 堆(Heap)是 GC 管理的主要区域,几乎所有 Java 对象实例都在这里分配。堆被划分为两个主要代(Generation):新生代(Young Generation)和老年代(Old Generation)。这种分代设计基于“弱分代假说”(Weak Generational Hypothesis):绝大多数对象生命周期极短,而存活下来的对象会趋向于长期存在。
| 区域 | 默认占比(以 Parallel Scavenge 为例) | 主要存储内容 | GC 频率 | GC 停顿特点 |
|---|---|---|---|---|
| 新生代 | 堆的 1/3 | 新创建的对象、生命周期短的对象 | 高(Minor GC 频繁) | 停顿短(毫秒级),但频率高 |
| 老年代 | 堆的 2/3 | 长期存活的对象、大对象(直接进入) | 低(Major/Full GC 较少) | 停顿长(秒级甚至分钟级) |
| 元空间(Metaspace) | 本地内存(非堆) | 类元数据、方法信息 | 触发条件特殊 | Full GC 时可能触发 |
3.2 新生代与老年代:对象晋升路径
对象在堆中的移动遵循一条明确的路径:Eden → Survivor0 → Survivor1 → Old。新生代内部进一步细分为 Eden 区和两个 Survivor 区(S0、S1)。
- Eden 区: 绝大多数对象首先在 Eden 区分配。当 Eden 区满时,触发 Minor GC。
- Survivor 区: Minor GC 后存活的对象被复制到 S0 或 S1。每次 Minor GC,存活对象在 S0 和 S1 之间来回复制,同时年龄(Age)加 1。
- 晋升阈值: 当对象年龄达到
-XX:MaxTenuringThreshold(默认 15)时,或 Survivor 区空间不足时,对象晋升到老年代。 - 大对象直接进入老年代: 通过
-XX:PretenureSizeThreshold设置,超过该大小的对象直接在老年代分配,避免在新生代频繁复制。
3.3 GC 算法简介:标记-清除、标记-复制、标记-整理
不同的 GC 算法决定了停顿时间的长短和内存碎片的程度。Flyway 脚本执行场景下,理解这三种基础算法,有助于选择正确的 GC 策略。
3.3.1 标记-清除(Mark-Sweep)
最基础的算法,分为两个阶段:
- 标记: 从 GC Roots 出发,遍历所有可达对象并标记为存活。
- 清除: 遍历整个堆,回收未被标记的对象内存。
缺点: 产生大量内存碎片,可能导致后续大对象分配失败而提前触发 Full GC。
3.3.2 标记-复制(Mark-Copy)
将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当这一块用完了,就将还存活着的对象复制到另一块上面,然后再把已使用的内存空间一次清理掉。
- 优点: 实现简单,运行高效,无内存碎片。
- 缺点: 可用内存缩小为原来的一半,对象存活率较高时复制开销大。
- 应用: 现代 JVM 的新生代默认采用此算法(Eden:S0:S1 = 8:1:1)。
3.3.3 标记-整理(Mark-Compact)
标记过程与“标记-清除”一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
- 优点: 无内存碎片,内存利用率高。
- 缺点: 移动对象需要 STW 停顿,且移动成本较高。
- 应用: 老年代 GC(如 CMS 的 Remark 阶段、G1 的 Mixed GC 中的部分场景)。
| 算法 | 内存碎片 | 吞吐量 | STW 停顿 | 适用区域 | Flyway 场景影响 |
|---|---|---|---|---|---|
| 标记-清除 | 严重 | 中等 | 中等 | 老年代(CMS) | 碎片导致大对象分配失败,触发 Full GC |
| 标记-复制 | 无 | 高 | 短 | 新生代 | 频繁 Minor GC,但停顿短,影响较小 |
| 标记-整理 | 无 | 低 | 长 | 老年代 | 长停顿直接拖慢脚本执行 |
3.4 与 Flyway 迁移脚本的关联总结
核心结论: Flyway 脚本执行慢,往往不是因为 SQL 执行本身,而是因为 GC 停顿。具体表现为:
- 新生代过小: 频繁 Minor GC,导致脚本线程频繁被 STW 打断。
- 对象过早晋升: 大 ResultSet 或长字符串直接进入老年代,触发 Major GC 长停顿。
- 老年代碎片化: 标记-清除算法导致碎片,分配大对象时触发 Full GC。
下一节将深入分析如何通过 GC 日志定位这些停顿,并给出具体的 JVM 参数调优方案。