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 时可能触发
Flyway 关联点: 迁移脚本执行时,大量 ResultSet、PreparedStatement、字符串拼接对象会在新生代快速创建。如果新生代空间不足,会触发频繁的 Minor GC;若对象过早晋升到老年代,则可能引发 Major GC 停顿,直接拖慢脚本执行。

3.2 新生代与老年代:对象晋升路径

对象在堆中的移动遵循一条明确的路径:Eden → Survivor0 → Survivor1 → Old。新生代内部进一步细分为 Eden 区和两个 Survivor 区(S0、S1)。

  1. Eden 区: 绝大多数对象首先在 Eden 区分配。当 Eden 区满时,触发 Minor GC。
  2. Survivor 区: Minor GC 后存活的对象被复制到 S0 或 S1。每次 Minor GC,存活对象在 S0 和 S1 之间来回复制,同时年龄(Age)加 1。
  3. 晋升阈值: 当对象年龄达到 -XX:MaxTenuringThreshold(默认 15)时,或 Survivor 区空间不足时,对象晋升到老年代。
  4. 大对象直接进入老年代: 通过 -XX:PretenureSizeThreshold 设置,超过该大小的对象直接在老年代分配,避免在新生代频繁复制。
JVM 堆内存结构:对象晋升路径 JVM 堆内存结构:对象晋升路径 新生代 (Young Generation) Eden 新对象分配 S0 存活对象 S1 存活对象 老年代 (Old Generation) Old 长期存活对象 大对象直接进入 大对象直接晋升 Minor GC 在新生代频繁发生,对象在 S0/S1 间复制,年龄达标后晋升至老年代

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,但停顿短,影响较小
标记-整理 无 低 长 老年代 长停顿直接拖慢脚本执行
三种 GC 算法对比:标记-清除 / 标记-复制 / 标记-整理 三种 GC 算法对比 标记-清除 存活对象 垃圾对象 碎片空间 产生碎片,分配大对象困难 标记-复制 From 空间 To 空间 存活对象复制到 To 空间 无碎片,但内存利用率减半 标记-整理 A B C 紧凑排列,无碎片 移动对象,STW 停顿长 Flyway 脚本执行中的 GC 算法选择建议 • 新生代:标记-复制(默认)—— 适合大量短生命周期对象,停顿短 • 老年代:标记-整理(G1/ZGC 优化版)—— 避免碎片,减少 Full GC 频率 • 避免使用 CMS 的标记-清除:碎片化会导致大对象分配失败,引发连续 Full GC • 调优目标:减少对象晋升速率,让 Minor GC 在新生代完成大部分回收

3.4 与 Flyway 迁移脚本的关联总结

核心结论: Flyway 脚本执行慢,往往不是因为 SQL 执行本身,而是因为 GC 停顿。具体表现为:

  • 新生代过小: 频繁 Minor GC,导致脚本线程频繁被 STW 打断。
  • 对象过早晋升: 大 ResultSet 或长字符串直接进入老年代,触发 Major GC 长停顿。
  • 老年代碎片化: 标记-清除算法导致碎片,分配大对象时触发 Full GC。

下一节将深入分析如何通过 GC 日志定位这些停顿,并给出具体的 JVM 参数调优方案。