JVM中对象如何从伊甸园区(Eden)进入幸存者区(Survivor)
在 JVM 的垃圾回收(GC)过程中,对象从 伊甸园区(Eden) 进入 幸存者区(Survivor) 的过程是 Minor GC(年轻代垃圾回收) 的核心机制。下面详细解释这一过程的步骤、条件和底层原理。
1. 对象分配流程(进入 Eden 区)
新对象优先分配在 Eden 区: 当程序通过 new 关键字创建对象时,JVM 会尝试在 Eden 区 分配内存。
如果 Eden 区空间不足,触发 Minor GC(年轻代垃圾回收)。如果 Minor GC 后仍不足,可能直接晋升到老年代(取决于 JVM 配置)。
2. Minor GC 触发时:对象从 Eden → Survivor
当 Eden 区满 时,JVM 会触发 Minor GC,存活的对象会被转移到 Survivor 区。具体步骤如下:
步骤 1:标记存活对象
可达性分析(GC Roots): JVM 从 GC Roots(如线程栈帧的局部变量、静态变量等)出发,标记所有 可达对象(存活对象)。
Eden 区中 被引用 的对象会被标记为存活。未被引用 的对象(垃圾)会被回收。
步骤 2:复制存活对象到 Survivor 区
存活对象从 Eden 复制到 Survivor(To 区):
所有 Eden 区存活的对象会被复制到 To Survivor(S0 或 S1)。对象年龄(Age) +1:每经历一次 Minor GC,对象的年龄增加 1(存储在对象头中)。Survivor 区采用“复制算法”:
每次 Minor GC 时,存活对象会被复制到 空的 Survivor 区(From 和 To 交替使用)。例如:
第一次 GC:Eden → S1(假设 S0 为空)。第二次 GC:Eden + S1 → S0(S1 变为 From,S0 变为 To)。
步骤 3:清空 Eden 区和 From Survivor
Eden 区和 From Survivor 被清空:
复制完成后,Eden 区和 当前的 From Survivor(存放上次存活对象的 Survivor)会被完全清空。From 和 To 角色互换:
原来的 To Survivor 变成下一轮的 From Survivor。原来的 From Survivor 变成下一轮的 To Survivor(等待接收新存活对象)。
3. 对象晋升老年代的条件
如果对象在 Survivor 区存活足够长时间,最终会晋升到 老年代(Old Generation)。晋升条件包括:
年龄阈值(MaxTenuringThreshold):
默认 -XX:MaxTenuringThreshold=15(经历 15 次 Minor GC 后晋升)。可通过 JVM 参数调整,例如 -XX:MaxTenuringThreshold=5。 动态年龄计算:
如果某一年龄(如 age=2)的对象总大小超过 Survivor 区的一半,则 ≥该年龄的对象直接晋升(避免 Survivor 区溢出)。 Survivor 区空间不足:
如果 Survivor 区无法容纳所有存活对象,部分对象会直接晋升到老年代(即使年龄未达标)。
4. 示例流程(Eden → Survivor → Old)
假设初始状态:
Eden 区:存放新对象 A, B, C。From Survivor(S0):空。To Survivor(S1):空。
第一次 Minor GC:
标记存活对象:假设 A, C 存活,B 被回收。复制到 To Survivor(S1):A, C 被复制到 S1,年龄 +1(A.age=1, C.age=1)。清空 Eden 和 From Survivor:Eden 和 S0 被清空。角色互换:
S1 变为 From Survivor(存放存活对象)。S0 变为 To Survivor(等待下次 GC)。
第二次 Minor GC:
Eden 区新分配对象 D, E, F。标记存活对象:假设 D, F 存活,E 被回收;S1 中的 A, C 仍然存活。复制到 To Survivor(S0):A, C, D, F 复制到 S0,年龄 +1(A.age=2, C.age=2, D.age=1, F.age=1)。清空 Eden 和 From Survivor(S1)。角色互换:
S0 变为 From Survivor。S1 变为 To Survivor。
后续晋升:
如果 A.age=15(默认阈值),则 A 会在下次 GC 时晋升到 老年代。
5. 关键 JVM 参数
参数作用建议值-XX:SurvivorRatioEden 区与单个 Survivor 区的比例(默认 8)。-XX:SurvivorRatio=6(增大 Survivor)。-XX:MaxTenuringThreshold对象晋升老年代的年龄阈值(默认 15)。-XX:MaxTenuringThreshold=5(减少年轻代停留)。-XX:+PrintGCDetails打印 GC 详情(观察 Eden/Survivor 变化)。调试时启用。-XX:PretenureSizeThreshold大对象直接进入老年代的阈值(默认 0)。-XX:PretenureSizeThreshold=1M(避免大对象占用 Eden)。
6. 常见问题
Q1:为什么 Survivor 区要分为 From 和 To?
复制算法(Copying Algorithm)要求:
每次 Minor GC 时,存活对象被复制到 空的 Survivor 区(To),然后清空原来的 Survivor 区(From),避免内存碎片。From 和 To 交替使用,确保始终有一个 Survivor 区是空的。
Q2:如果 Survivor 区空间不足怎么办?
直接晋升老年代:
如果 Survivor 区无法容纳所有存活对象,部分对象会 直接进入老年代(即使年龄未达标)。可通过 -XX:SurvivorRatio 调整比例,或增大年轻代总大小(-Xmn)。
Q3:如何监控对象年龄分布?
使用 jstat 或 -XX:+PrintTenuringDistribution:jstat -gc
或-XX:+PrintTenuringDistribution # 打印对象年龄分布
总结
Eden → Survivor:Minor GC 时,存活对象从 Eden 复制到 To Survivor,年龄 +1。Survivor 区交替使用:From 和 To 在每次 GC 后交换角色。晋升老年代:年龄 ≥ MaxTenuringThreshold 或 Survivor 空间不足时晋升。调优关键:通过 SurvivorRatio 和 MaxTenuringThreshold 优化对象生命周期。
通过 jvisualvm 或 GC 日志 可以直观观察对象在 Eden/Survivor 区的流动情况。