Skip to content

源码: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++;
        }
    }
}

站点构建时间:2026/8/24 23:43:17