简单说,Java 程序运行时会在 JVM 堆内存里创建大量对象(比如new User()),那些不再被任何引用指向的对象就是 “垃圾”,如果不清理,内存会被占满导致程序崩溃。垃圾回收的核心目标:自动识别并清理堆中无用对象,释放内存,避免内存泄漏 / 溢出,不用像 C++ 那样手动free/delete。
这是 GC 的第一步,常用 2 种方法:
- 逻辑:给每个对象加一个 “引用计数器”,有引用指向它就 + 1,引用断开就 - 1;计数器为 0 就是垃圾。
- 坑:无法解决循环引用(比如 A 引用 B,B 引用 A,两者都没用但计数器都不为 0),JVM 实际不用这种方法。
- 逻辑:以 “GC Roots” 为起点(比如虚拟机栈里的局部变量、静态变量、JNI 引用等),向下遍历引用链;没有被连接到的对象就是垃圾。
- 举个栗子:
public void test() {
User u1 = new User(); // u1是GC Roots(局部变量),User对象可达,不是垃圾
u1 = null; // 引用断开,User对象不可达,标记为垃圾
User u2 = new User();
User u3 = u2;
u2 = null; // u3还引用User对象,仍可达,不是垃圾
}
JVM 把堆分成 “新生代” 和 “老年代”,因为不同对象的生命周期不同,用不同策略回收更高效:
新生代又分 1 个 Eden 区 + 2 个 Survivor 区(S0/S1):
- 新对象先放 Eden 区,Eden 满了触发 Minor GC,存活对象复制到 S0,清空 Eden;
- 下次 Minor GC,Eden+S0 的存活对象复制到 S1,清空 Eden+S0;
- 反复几次后,还存活的对象 “晋升” 到老年代;
- 老年代满了触发 Major GC/Full GC,清理老年代垃圾(耗时久,会导致程序卡顿)。
不同回收器适配不同场景,核心看 “吞吐量” 和 “停顿时间”:
- 单线程回收,只适合单核 / 小内存场景(比如桌面程序、嵌入式),优点是简单、内存占用少,缺点是停顿久。
- 多线程回收新生代,追求 “高吞吐量”(运行程序时间 / 总时间),适合后台计算型程序(比如大数据处理),默认 JDK8 的新生代回收器。
- 老年代回收器,主打 “低停顿”(用户几乎感知不到 GC),分 4 步:初始标记→并发标记→重新标记→并发清除;
- 缺点:会产生内存碎片,占用 CPU 资源,JDK9 开始被标记为废弃。
- JDK9 默认回收器,把堆分成多个小块,优先回收垃圾多的块,兼顾吞吐量和停顿时间,适合大内存场景(比如 8G 以上堆),是目前主流。
- 误区 1:GC 会回收所有无用对象 → 错!GC 只能回收堆内存的对象,栈、方法区的资源(比如线程、静态变量)管不了;
- 误区 2:Full GC 是手动调用
System.gc()触发的 → 错!System.gc()只是 “建议” JVM 回收,JVM 可以忽略;Full GC 通常是老年代满、永久代 / 元空间满等触发;
- 误区 3:GC 停顿越短越好 → 错!要平衡:比如实时系统(电商支付)要低停顿,后台计算要高吞吐量。
- JVM 垃圾回收核心是识别无用对象(可达性分析)+ 分代清理(新生代 / 老年代),避免手动管理内存;
- 判断垃圾的核心算法是可达性分析,而非引用计数(解决循环引用问题);
- 常见回收器各有侧重:Serial(单核)、Parallel(吞吐量)、CMS(低停顿)、G1(兼顾),实际开发中 G1 是主流选择。