System.gc()仅是向JVM发出垃圾回收建议,不保证立即执行、不决定GC类型与时机;开启-XX:+DisableExplicitGC时会被完全忽略,且不影响JVM自发GC。

System.gc() 不是回收开关,而是一个可控的观察切口——它本身不决定回收是否发生、何时发生、回收哪些对象,但在人为设定内存边界(如固定堆大小、禁用自动扩容)的前提下,能帮你看清 JVM 在压力下的真实调度逻辑。
先设好“牢笼”:用 JVM 参数锁死内存空间
想让 System.gc() 的效果可测,必须排除内存自动伸缩的干扰。典型配置如下:
- -Xms200m -Xmx200m:堆初始与最大值一致,避免动态扩容掩盖回收行为
- -Xmn100m:年轻代固定为 100MB,Eden 区约 88MB(默认 SurvivorRatio=8),便于触发 Minor GC
- -XX:+PrintGCDetails -Xloggc:gc.log:记录每次 GC 类型、耗时、各代变化,是判断是否生效的唯一依据
- -XX:+UseSerialGC 或 -XX:+UseG1GC:Serial 易于观察线性行为;G1 则需配合 -XX:+ExplicitGCInvokesConcurrent 才可能响应 System.gc()
写一段“压力代码”,制造可追踪的大对象
不要用小对象测试,它们容易被 JIT 优化或直接栈上分配。推荐构造明确、易识别的内存占用:
- 分配一个 64MB 的 byte[],确保它大概率直接进入老年代(超过 -XX:PretenureSizeThreshold 或 Eden 空间不足)
- 用 局部作用域 + 显式置 null 控制引用生命周期,例如:
{ byte[] arr = new byte[64 * 1024 * 1024]; arr = null; } - 在作用域结束后立即调用 System.gc(),再 sleep 等待 GC 完成
- 避免在循环中调用,否则 GC 日志会被淹没,且可能触发 JVM 主动降级该调用
看日志,而不是看 freeMemory()
Runtime.getRuntime().freeMemory() 返回的是 JVM 认为“当前可用”的堆内存,但这个值受内存整理、碎片、GC 类型影响极大,几乎无法反映真实回收结果。真正可靠的信号只有 GC 日志:
- 出现 Full GC (System) 表示该次 Full GC 由代码显式触发,不是内存压力自然引发
- 对比 GC 前后 OldGen 使用量:若下降明显(如 -64MB),说明大数组被成功回收
- 若只看到 GC (Allocation Failure) 而无 (System),说明 JVM 忽略了你的调用(常见于 G1/ZGC 默认配置)
- 日志中出现 Metaspace 或 Compressed Class Space 变化,说明类加载器相关对象也被清理,属于附带效应
别信“立刻释放”,要理解回收的异步性
即使日志显示 Full GC (System) 成功执行,也不代表操作系统立刻收回物理内存。JVM 通常保留已释放的堆内存供后续分配复用,除非启用 -XX:+AlwaysPreTouch 或 -XX:+UseZGC 等特殊策略。你看到的“回收”,本质是 JVM 内部标记空间可重用,而非向 OS 交还页帧。
真正稳定的回收表现,来自对象不可达 + 合适的 GC 周期 + 无跨代引用干扰。System.gc() 只是帮你把“合适时机”往前推一小步,而不是绕过整个机制。


















