from survivor和to survivor的切换由jvm在minor gc(新生代垃圾回收)执行完毕后自动完成,具体做法是复制存活对象并交换两个survivor区的引用指向。这一过程不需要开发者手动干预,只需关注gc日志或堆内存状态即可确认切换结果。若遇到full gc或老年代空间不足导致提前晋升,survivor区的状态也会随之改变,但核心切换逻辑依然遵循复制算法的规则。

为什么需要两个Survivor区
HotSpot虚拟机默认使用复制算法来清理新生代,将内存分为一块较大的Eden区和两块较小的Survivor区,比例通常为8:1:1。之所以设计两个Survivor区而非一个,是为了避免内存碎片化并提高回收效率。如果只有一个Survivor区,存活对象需要复制到同一块区域的不同位置,这会导致内存不连续,增加管理复杂度。而双Survivor区允许每次只使用其中一个进行分配,另一个作为空白目标,回收时直接将存活对象复制到空白区,原区域整个清空,无需整理内存。

public class SurvivorSwapDemo {
public static void main(String[] args) {
// 设置堆大小以便观察,实际生产中通常由JVM自动管理
// -Xms10m -Xmx10m -XX:+PrintGCDetails -XX:+UseSerialGC
byte[] allocation1, allocation2, allocation3;
allocation1 = new byte[1 * 1024 * 1024]; // 1MB Eden
allocation2 = new byte[1 * 1024 * 1024]; // 1MB Eden
allocation3 = new byte[1 * 1024 * 1024]; // 1MB Eden
// 触发Minor GC
System.gc();
// 此时Eden区被清空,存活对象移动到To Survivor
// From Survivor和To Survivor的角色已经互换
allocation4 = new byte[1 * 1024 * 1024]; // 再次分配
System.gc(); // 再次触发GC,角色再次互换
}
}
上述代码通过连续分配大数组触发垃圾回收。第一次GC后,Eden中的存活对象被复制到当前的To Survivor区,原来的To Survivor变为From Survivor。第二次GC时,原本在Survivor中的对象若仍存活,会被复制到另一个Survivor区,实现角色轮换。这种机制确保了Survivor区始终有一块是空的,用于接收下一次GC的存活对象。
立即学习“Java免费学习笔记(深入)”;
Survivor区满了会发生什么
当Survivor区容量不足以容纳一次Minor GC后所有存活对象时,JVM会启动空间担保机制。此时,部分对象会直接晋升到老年代,而不是继续在Survivor区之间切换。如果老年代也空间不足,则会触发Full GC。此外,如果对象年龄达到阈值(默认为15岁),也会强制晋升到老年代,不再参与Survivor区的切换。这意味着Survivor区的切换并非无限循环,而是受限于内存空间和对象生命周期。

// 模拟Survivor区满的情况
public class SurvivorOverflowDemo {
public static void main(String[] args) {
// 假设Survivor区很小,例如1MB
// 分配大量短命对象填满Eden
for (int i = 0; i < 100; i++) {
byte[] data = new byte[100 * 1024]; // 100KB
// 这些对象在下次GC时大部分死亡
}
System.gc();
// 分配长命对象,迫使它们进入Survivor并可能晋升
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 50; i++) {
list.add(new byte[100 * 1024]);
}
// 多次GC后,这些对象因年龄增长或Survivor溢出而晋升到老年代
while(true) {
System.gc();
try { Thread.sleep(100); } catch (InterruptedException e) {}
}
}
}
该示例展示了Survivor区满时的行为。当存活对象数量超过Survivor区容量时,多余的对象会直接晋升到老年代。这改变了正常的From/To切换流程,因为对象不再在两个Survivor区之间来回移动,而是直接进入老年代。开发者可以通过调整-XX:MaxTenuringThreshold参数来控制对象晋升的年龄阈值,从而影响Survivor区的切换频率和老年代的压力。
如何监控Survivor切换状态
要观察From Survivor和To Survivor的实际切换情况,最直接的方法是查看JVM的GC日志。启用-XX:+PrintGCDetails参数后,GC日志会详细记录每次Minor GC后各区域的内存使用情况。通过对比两次GC前后的Survivor区占用量,可以推断出哪一个是当前的From Survivor,哪一个是To Survivor。此外,使用jstat工具也可以实时监控新生代各区的状态,特别是S0U和S1U字段,分别代表Survivor 0和Survivor 1的使用量。
jstat -gcutil <pid> 1000
运行上述命令后,输出中的S0U和S1U列显示了两个Survivor区的使用百分比。如果S0U高而S1U低,说明当前From Survivor是S0,To Survivor是S1。下一次GC后,情况可能会反转。通过持续监控这两个值的变化,可以清晰地看到Survivor区的切换过程。这种方法适用于生产环境的故障排查和性能调优,帮助开发者理解JVM内部的内存管理机制。


















