JVM的GC触发条件与堆分区对象变化
在JVM中,GC(垃圾回收)的触发时机和对象变化取决于具体的垃圾回收器实现,但我们可以从分代收集理论的角度来理解通用的规则。
分代收集理论
一、GC的触发时机
JVM的GC主要分为两种:Minor GC(小型GC)和Full GC(完全GC)(或称Major GC)。
1. Minor GC 的触发条件
- 触发场景:当新生代(Young Generation)的Eden区(伊甸园区)被占满,无法为新对象分配内存时。
- 特点:通常发生得非常频繁,回收速度也很快。
2. Full GC 的触发条件
Full GC涉及对整个堆(包括新生代、老年代和方法区/元空间)的回收,触发条件相对复杂:
- 老年代空间不足:
- 对象从新生代升入老年代时,发现老年代剩余空间不足以容纳这些对象。
- 大对象(如很长的数组)直接在老年代分配时,老年代空间不足。
- 空间分配担保失败:
- Minor GC后,存活对象大小超过了幸存区(Survivor)的容量,需要进入老年代,但老年代无法容纳(或担保失败)。
- 元空间(Metaspace)不足:
- 加载的类太多,或者使用了大量的动态代理/CGLIB,导致元空间内存耗尽。
- 显式调用:
- 在代码中调用
System.gc()(除非 JVM 显式设置了-XX:+DisableExplicitGC来禁止)。
- 在代码中调用
- JVM工具或JMX触发:
- 通过JMX等管理接口触发的GC。
二、GC发生时,堆内存分区中的实例对象变化
堆内存主要分为:新生代(Eden、Survivor From、Survivor To)和老年代(Old Generation)。
1. Minor GC 发生时的变化(以复制算法为主)
假设初始状态:
- Eden区:存满了对象(包括存活的和垃圾)。
- Survivor From:存有上一轮存活下来的对象。
- Survivor To:为空。
回收过程与对象变化:
- 标记:扫描Eden区和Survivor From区,标记出哪些对象是存活的。
- 复制与年龄增加:
- Eden区中的存活对象,以及Survivor From区中的存活对象,会被复制到空的 Survivor To 区。
- 每复制一次,这些对象的年龄计数器就增加1。
- 规则:如果对象的年龄达到了阈值(例如15,取决于JVM参数),则直接晋升(Promotion)到老年代,而不是复制到Survivor To。
- 清理:直接清空Eden区和Survivor From区(此时里面全是垃圾对象)。
- 角色互换:Survivor From 和 Survivor To 交换指针(下次GC时,To区变为From区,空的区域成为新的To区)。
结果:
- 垃圾对象:被彻底回收,内存被释放。
- 存活对象:移动到了 Survivor To 或 老年代。
2. Full GC 发生时的变化(以标记-清除-压缩/复制算法为例)
Full GC通常会回收整个堆(新生代+老年代),甚至包括元空间。
回收过程与对象变化:
- 初始标记:暂停所有用户线程(Stop The World),标记出GC Roots直接关联的对象。
- 并发标记:从GC Roots开始,遍历整个对象图,标记所有存活对象(这一般是并发执行的)。
- 重新标记:修正并发标记期间因用户线程继续运行而产生变动的标记记录。
- 清除与压缩:
- 新生代:依然采用复制算法,清空Eden和Survivor。
- 老年代:由于老年代空间较大,通常采用标记-清除-压缩算法。
- 标记:标记存活对象。
- 清除:回收垃圾对象占用的内存。
- 压缩:将所有存活对象向内存空间的一端移动,消除内存碎片。
结果:
- 垃圾对象:被完全清理。
- 存活对象:被移动到堆内存的起始位置(压缩后),使得剩余空闲内存是连续的一大块。
三、对象变化的核心规则总结
| 场景 | 对象变化 |
|---|---|
| 刚创建的对象 | 分配在 Eden 区 |
| 经历一次 Minor GC 后存活 | 从 Eden 移动到 Survivor To(年龄+1) |
| 多次 Minor GC 后存活 | 在 Survivor 区之间来回移动(年龄持续增加) |
| 年龄达到阈值 / 动态年龄判定 | 从 Survivor 晋升到 老年代 |
| 老年代空间不足触发 Full GC | 老年代存活对象被压缩整理,垃圾被清理 |
| 大对象直接分配 | 直接进入 老年代 |
简单来说: GC的过程就像是一个“整理房间”的过程。新生代的GC像是一个**“分类垃圾桶”,先把有用的东西挑出来放到旁边的空桶(Survivor To),然后把原来的桶倒掉(清空Eden)。老年代的GC更像一个“仓库盘点”**,把没用的废品扔掉,然后把剩下的货物堆整齐(压缩),腾出空间。
主流垃圾收集器
上面描述的规则(新生代复制、老年代标记-整理)确实是基于分代收集理论的通用设计思想。但在具体的垃圾收集器(Garbage Collector, GC)实现中,为了追求不同的目标(如低延迟、高吞吐量或大内存管理),这个理论的落地方式会有很大的差别,甚至有些收集器已经不完全遵循传统的分代物理划分。
主要的差别体现在内存布局、回收算法以及回收阶段上。以下是主流垃圾收集器与标准分代理论的差异对比:
1. CMS(Concurrent Mark-Sweep)收集器
目标: 获取最短回收停顿时间(低延迟)。
- 与理论的差异:
- 算法不同: 标准理论中老年代通常使用标记-整理(压缩)算法,但CMS在老年代使用的是标记-清除算法。
- 后果: 由于不压缩,CMS 不会移动对象。这虽然减少了停顿时间,但会产生内存碎片。
- 触发机制变化: 由于内存碎片的存在,即使老年代总空间还很大,但如果没有连续空间容纳从新生代晋升的大对象,就会触发 Concurrent Mode Failure,导致退化为使用 Serial Old 收集器进行 Full GC(这时就会进行压缩整理,停顿时间极长)。
2. G1(Garbage First)收集器
目标: 在延迟可控的前提下实现高吞吐量,面向大堆内存。
- 与理论的差异:
- 物理分区消失: G1 不再严格坚持对象在物理上必须待在“新生代”或“老年代”。它将整个堆划分为多个大小相等的 Region(区域)。
- 逻辑分代仍在: 虽然物理上不分代,但每个 Region 会在逻辑上被标记为 E、S、O(Eden、Survivor、Old)。
- 算法混合: G1 整体上属于 标记-整理 算法(从整体看是整理,局部看是复制)。
- 回收范围变化: G1 引入了 Mixed GC(混合GC)。它不再只是单独回收新生代或全堆,而是在一次 GC 中既处理新生代,也处理部分老年代 Region(根据收集优先级,优先回收垃圾最多的 Region)。
3. ZGC(Z Garbage Collector)收集器
目标: 极低延迟(<10ms),支持超大堆内存(TB级),且停顿时间不随堆大小增加。
- 与理论的差异:
- 分代实验特性: 早期的 ZGC 是不分代的!它同时回收新生代和老年代的所有对象。这与最基础的分代理论完全相反。
- 染色指针与读屏障: ZGC 通过指针技术实现并发整理。它在对象移动时,通过读屏障和染色指针技术,允许用户线程在读对象时如果发现对象被移动了,可以立即帮助修复引用(类似于“自愈”),而不用等待垃圾收集线程处理。
- 内存布局: ZGC 也使用 Region(但称为 Page),但具有动态创建和销毁的特性,且 Region 大小可以动态变化(大、中、小三类)。
4. Epsilon 收集器
目标: 无操作收集器(主要用于性能测试、短生命周期的服务)。
- 与理论的差异:
- 根本不回收! 它处理内存分配,但当堆内存耗尽时,JVM 直接崩溃。这完全跳过了所有的 GC 理论和算法,用于在极端情况下测试应用本身的内存使用边界。
5. Shenandoah 收集器
目标: 与 ZGC 类似,极低延迟。
- 与理论的差异:
- 并发整理: 传统的理论中,对象移动(复制/整理)通常需要暂停用户线程。Shenandoah 与 ZGC 一样,实现了并发整理,让对象移动的阶段也能与用户线程一起运行。
- 转发指针: 它通过在对象头中引入转发指针来实现,与 ZGC 的染色指针实现方式不同。
总结:理论 vs. 现实的差异点
为了让你更直观地理解,可以看看下面这个对比表格:
| 比较维度 | 经典分代理论 | CMS | G1 | ZGC (早期) |
|---|---|---|---|---|
| 老年代算法 | 标记-整理 | 标记-清除 | 标记-整理 | 标记-整理 |
| 物理分代 | 是 | 是 | 逻辑分代,物理不分 | 通常不分代 |
| 内存布局 | 连续 Eden/S0/S1/Old | 连续 Eden/S0/S1/Old | Region 化 | 动态 Region |
| 并发阶段 | 较少 | 并发标记 | 并发标记 | 并发标记+并发整理 |
| 主要缺陷 | 无(基础模型) | 内存碎片、CPU高 | 算法复杂、需预测 | 内存占用略高 |
简单来说:
- 如果你使用的是 Serial、ParNew、Parallel Scavenge,它们基本遵循你之前了解的那套经典分代理论。
- 如果你使用的是 CMS,它打破了老年代必须整理(压缩)的理论。
- 如果你使用的是 G1,它打破了堆必须物理分代的理论。
- 如果你使用的是 ZGC,它甚至打破了必须分代(早期版本)以及必须暂停才能移动对象的理论。
这些实现上的差异,本质上都是为了在吞吐量、延迟和内存占用这三者之间寻找更适合特定场景的平衡点。