Appearance
源码:labs/java/memory-lifecycle/gc-algorithms-demo/src/GcAlgorithmsDemo.java
- 原始路径:
labs/java/memory-lifecycle/gc-algorithms-demo/src/GcAlgorithmsDemo.java - 类型:
java
java
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryPoolMXBean;
import java.util.List;
/**
* JVM GC 算法可观察对照实验(纯 JDK,零三方依赖)。
*
* 配套知识库文档:docs/02-memory-lifecycle/05-jvm-gc-algorithms.md
*
* 三个场景与文档口径的对应关系:
* - 场景 1(短命对象高频分配)→ 弱分代假说 + 年轻代复制算法(Eden → Survivor 反复拷贝),
* 对应文档「常见场景 2:避免在 onDraw 或高频 Loop 中分配内存」。
* - 场景 2(大对象绕过年轻代)→ Serial 下由 -XX:PretenureSizeThreshold 直入老年代;
* G1 下表现为超过 region 一半大小的 Humongous 对象直接占用连续 Old Region;
* 对应文档「常见场景 1:大对象直接进入老年代 / Large Object Space」。
* - 场景 3(显式 System.gc 对照)→ Full GC 标记-整理前后老年代用量回落印证压缩整理;
* 互相引用的“孤岛”对象被回收印证 GC Roots 可达性分析(引用计数无法做到)。
*/
public class GcAlgorithmsDemo {
private static final List<MemoryPoolMXBean> POOLS = ManagementFactory.getMemoryPoolMXBeans();
private static final List<GarbageCollectorMXBean> GCS = ManagementFactory.getGarbageCollectorMXBeans();
/** 写入静态字段的“黑洞”:阻止 JIT 逃逸分析把实验里的分配整体优化掉。 */
private static byte[] sink;
private static int failures = 0;
/** 互相引用的“孤岛”节点:引用计数算法永远清不掉,只有可达性分析能回收。 */
static class Island {
Island peer; // peer 成环:a.peer == b, b.peer == a
final byte[] payload = new byte[1024 * 1024]; // 1MB 载荷,便于从内存用量上观察到回收
Island(Island peer) {
this.peer = peer;
}
}
public static void main(String[] args) {
boolean g1 = isG1();
System.out.println("[demo] 检测到垃圾收集器: " + collectorNames());
System.out.println("[demo] 场景 2 按 " + (g1
? "G1 口径解读:2MB > region(1m) 一半 => Humongous 直入 Old Region"
: "Serial 口径解读:2MB > PretenureSizeThreshold(1m) => 直接进 Tenured Gen")
+ ",两种路径都不经过 Eden");
scenario1ShortLivedChurn();
scenario2LargeObjects(g1);
scenario3ExplicitGc(g1);
System.out.println();
if (failures == 0) {
System.out.println("PASS: 全部检查通过");
System.exit(0);
} else {
System.out.println("FAIL: 有 " + failures + " 项检查未通过,请结合上方输出与 -Xlog:gc* 日志排查");
System.exit(1);
}
}
/** 场景 1:30 万个 1KB 朝生夕死对象(约 300MB 总量),反复填满 Eden 触发多次 Minor GC。 */
private static void scenario1ShortLivedChurn() {
banner("场景 1: 短命对象高频分配 -> 弱分代假说 + 年轻代复制算法");
long youngBefore = youngCollections();
long oldBefore = poolUsedMb("Old");
// junk 出循环体即刻不可达:绝大多数对象撑不过下一次 Minor GC —— 这就是弱分代假说。
// 引用必须写进静态字段 sink,否则 JIT 会用逃逸分析把“不逃逸的分配”整体消掉,实验就失真了。
// Minor GC 采用复制算法:把 Eden + From Survivor 里少量存活对象拷到 To Survivor 后整块清空 Eden。
// 死亡比例越高复制成本越低 —— 新生代选复制算法正是因为这里“朝生夕死”的对象占绝大多数。
for (int i = 0; i < 300_000; i++) {
byte[] junk = new byte[1024];
junk[0] = (byte) i;
sink = junk;
}
long youngAfter = youngCollections();
System.out.printf("[demo] 场景1 共分配约 %d MB 短命对象, Minor GC 次数 %d -> %d (新增 %d 次)%n",
300_000 / 1024, youngBefore, youngAfter, youngAfter - youngBefore);
forceOneYoungGc(); // 清掉循环结束时的 Eden 残留,给场景 2 一个干净基线
long oldAfter = poolUsedMb("Old");
System.out.printf("[demo] 补一次 Minor GC 后 Eden 已用 %d MB、老年代 %d MB —— 短命垃圾几乎全部死在年轻代, 未晋升%n",
poolUsedMb("Eden"), oldAfter);
check(youngAfter - youngBefore >= 2, "场景1: 高频分配应触发至少 2 次 Minor GC");
check(oldAfter - oldBefore <= 4, "场景1: 朝生夕死对象几乎不应晋升老年代(增量 <= 4MB)");
}
/** 场景 2:40 个 2MB 大对象用完即弃,观察它们是否绕过 Eden 直接落在老年代。 */
private static void scenario2LargeObjects(boolean g1) {
banner("场景 2: 大对象直接进老年代 (Serial: PretenureSizeThreshold / G1: Humongous Region)");
forceOneYoungGc(); // 先把年轻代清干净,排除场景 1 残留干扰
long tenuredBefore = poolUsedMb("Old");
long youngBeforeLoop = youngCollections();
// 2MB 远大于 Serial 的 -XX:PretenureSizeThreshold=1048576,也大于 G1 region(1m) 的一半:
// 这类对象不走 Eden -> Survivor 复制流水线,而是直接在老年代的连续空间上分配,
// 与 ART 把大对象放进独立 Large Object Space 的思路互为对照。
for (int i = 0; i < 40; i++) {
byte[] big = new byte[2 * 1024 * 1024];
big[big.length - 1] = 1; // 写一个字节,防止被 JIT 当作无副作用分配优化掉
sink = big; // 同样过一遍黑洞,确保分配真实发生
}
long tenuredAfter = poolUsedMb("Old");
long edenAfter = poolUsedMb("Eden");
System.out.printf("[demo] 场景2 共分配 40 个 2MB 大对象(约 %d MB), 老年代已用 %d MB -> %d MB%n",
40 * 2, tenuredBefore, tenuredAfter);
System.out.printf("[demo] 场景2 结束后 Eden 已用量仅 %d MB —— 大对象基本没有经过年轻代%n", edenAfter);
if (g1) {
// G1 会在 Young GC 里“顺手”急切回收已死亡的 Humongous region(日志可见 Humongous regions: N->M),
// 所以老年代瞬时用量不会稳定停留在高位;确定性证据是分配压力本身触发了 GC。
check(youngCollections() - youngBeforeLoop >= 1,
"场景2(G1): Humongous 分配压力应触发至少一次 GC(日志 cause 为 G1 Humongous Allocation)");
} else {
check(tenuredAfter - tenuredBefore >= 48, "场景2(Serial): 大对象应直接推高老年代用量(增量 >= 48MB)");
}
check(edenAfter <= 16, "场景2: 大对象路径不应显著占用 Eden(<= 16MB)");
}
/** 场景 3:显式 System.gc() 触发 Full GC(标记-整理压缩),并验证循环引用孤岛被可达性分析回收。 */
private static void scenario3ExplicitGc(boolean g1) {
banner("场景 3: 显式 System.gc() 对照 -> 标记-整理 + 循环引用孤岛回收");
buildAndDropIslands();
long before = poolUsedMb("Old");
System.out.printf("[demo] System.gc() 前老年代已用 %d MB (含场景2遗留的死大对象 + 约 2MB 孤岛载荷)%n", before);
System.gc(); // 同步 Full GC:Serial 为单线程标记-整理(Mark-Compact),G1 为带压缩的 Full GC
long after = poolUsedMb("Old");
System.out.printf("[demo] System.gc() 后老年代已用 %d MB —— 死亡对象连同其碎片位置一起被压缩清理%n", after);
if (g1) {
// G1 下场景 2 的死大对象多半已被急切回收,Full GC 前后差值只剩孤岛等少量垃圾,
// 因此只要求“严格回落”即可;Serial 保留大差值断言。
check(after < before, "场景3(G1): Full GC 后老年代应严格回落");
} else {
check(after <= before - 32, "场景3(Serial): Full GC 后老年代应明显回落(释放 >= 32MB)");
}
System.out.println("[demo] 说明: 刚才回收的对象里有两个 Island 节点互相引用(peer 字段成环),");
System.out.println("[demo] 但方法返回后没有任何 GC Root 能到达这个环 —— 引用计数算法对它无能为力,");
System.out.println("[demo] 可达性分析从 Roots 出发扫不到它,于是整座孤岛随 Full GC 一起回收。");
}
/** 构造 a <-> b 互相引用的孤岛后立刻返回:局部变量出栈,环上再无任何 GC Root 入口。 */
private static void buildAndDropIslands() {
Island a = new Island(null);
Island b = new Island(a);
a.peer = b; // 成环:即使 b 还引用着 a,也救不了任何一个
}
/**
* 用小对象持续填充年轻代,直到刚好又发生一次 Young GC:
* 让 Eden 被复制算法整块清空,作为下一个场景的确定性基线(不依赖 System.gc)。
*/
private static void forceOneYoungGc() {
long target = youngCollections() + 1;
int guard = 0;
while (youngCollections() < target && guard++ < 5_000_000) {
byte[] pad = new byte[4096];
pad[0] = 1;
sink = pad;
}
}
private static boolean isG1() {
return GCS.stream().anyMatch(gc -> gc.getName().contains("G1"));
}
private static String collectorNames() {
StringBuilder sb = new StringBuilder();
for (GarbageCollectorMXBean gc : GCS) {
sb.append(gc.getName()).append(' ');
}
return sb.toString().trim();
}
/** 按角色取内存池已用量(MB):兼容 Serial 的 Eden Space/Tenured Gen 与 G1 的 G1 Eden Space/G1 Old Gen 命名。 */
private static MemoryPoolMXBean findPool(String role) {
for (MemoryPoolMXBean pool : POOLS) {
String name = pool.getName();
if ("Eden".equals(role) && name.contains("Eden")) {
return pool;
}
if ("Old".equals(role) && (name.contains("Tenured") || name.contains("Old Gen"))) {
return pool;
}
}
throw new IllegalStateException("找不到内存池: " + role);
}
private static long poolUsedMb(String role) {
return findPool(role).getUsage().getUsed() / (1024 * 1024);
}
/** 年轻代收集总次数:兼容 Serial(Copy)/Parallel(PS Scavenge)/G1(G1 Young Generation) 的 MXBean 命名。 */
private static long youngCollections() {
long sum = 0;
for (GarbageCollectorMXBean gc : GCS) {
String name = gc.getName();
if (name.contains("Copy") || name.contains("Scavenge") || name.contains("Young")) {
sum += gc.getCollectionCount();
}
}
return sum;
}
private static void banner(String title) {
System.out.println();
System.out.println("=============================================================");
System.out.println("== " + title);
System.out.println("=============================================================");
}
private static void check(boolean ok, String message) {
System.out.println((ok ? "[ok] " : "[FAIL] ") + message);
if (!ok) {
failures++;
}
}
}