垃圾回收器是Java语言实现自动内存管理的核心组件,它负责在后台自动回收不再被程序使用的对象所占用的内存空间,从而将开发者从繁琐且易错的手动内存管理中解放出来。
什么是垃圾回收器?
简单来说,垃圾回收器就是Java的自动内存管理机制。它持续监控堆内存中对象的生存状态,识别并回收那些已经“死亡”(即不再有任何引用指向它们)的对象,释放其占用的内存资源以供后续分配使用。
// 垃圾回收的简单示例
class Immortal {
private String name;
public Immortal(String name) {
this.name = name;
System.out.println(name + " 对象被创建");
}
@Override
protected void finalize() throws Throwable {
System.out.println(name + " 对象被垃圾回收");
super.finalize();
}
}
public class ReincarnationDemo {
public static void main(String[] args) {
Immortal taoist = new Immortal("示例对象");
taoist = null; // 对象失去引用,成为垃圾
// 建议JVM进行垃圾回收(但不保证立即执行)
System.gc();
// 给GC一点时间执行
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
为什么需要垃圾回收?
手动内存管理的挑战
在C/C++等语言中,开发者需要显式地分配和释放内存,这带来了常见的问题:
- 内存泄漏:忘记释放已分配的内存。
- 悬垂指针/二次释放:访问已释放的内存或重复释放同一块内存,导致程序行为不可预测或崩溃。
Java的解决方案:自动垃圾回收
Java通过引入垃圾回收器,让运行时环境(JVM)自动管理对象生命周期,极大地提升了开发效率和程序的健壮性。
垃圾回收器的工作原理
1. 判断对象是否存活
GC的首要任务是判断堆中的哪些对象可以被回收。它通过“可达性分析算法”来实现:以一系列称为“GC Roots”的对象(如虚拟机栈中引用的对象、静态属性引用的对象等)为起点,向下搜索。如果某个对象到GC Roots没有任何引用链相连,则证明此对象不可用,可以被回收。
2. 主流垃圾回收算法
标记-清除算法(Mark-and-Sweep)
这是最基础的算法,分为两个阶段:
- 标记:遍历所有GC Roots,标记所有可达对象。
- 清除:遍历整个堆,回收未被标记的对象所占用的空间。
缺点:会产生大量不连续的内存碎片。
复制算法(Copying)
将可用内存按容量分为大小相等的两块,每次只使用其中一块。当这一块用完时,就将还存活的对象复制到另一块上,然后把已使用的内存空间一次清理掉。
优点:实现简单,运行高效,没有碎片。
缺点:内存利用率只有一半。
标记-整理算法(Mark-Compact)
标记过程与“标记-清除”算法一样,但后续步骤不是直接回收,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。
优点:避免了碎片化问题,也无需牺牲一半内存。
缺点:移动对象成本较高。
分代收集算法(Generational Collection)
现代商用JVM普遍采用的策略。其核心思想是根据对象存活周期的不同,将堆内存划分为几块(通常是新生代和老年代),然后根据各年代的特点采用最合适的收集算法。这通常涉及到Java性能调优的深入实践。
- 新生代:对象“朝生夕死”,回收频繁,适合采用复制算法。
- 老年代:对象存活率高,没有额外空间进行分配担保,适合采用标记-清除或标记-整理算法。
深入JVM内存区域
理解GC需要对JVM内存布局有清晰认识:
- 堆:所有对象实例和数组都在堆上分配,是GC管理的主要区域。
- 虚拟机栈:存储局部变量表、操作数栈、动态链接、方法出口等信息。其中局部变量表存放了编译期可知的基本数据类型和对象引用。
- 方法区:用于存储已被虚拟机加载的类信息、常量、静态变量等数据。
关于finalize()方法
finalize()是Object类的一个受保护方法。当垃圾回收器确定不存在对该对象的更多引用时,会在回收对象内存之前调用此方法。
重要警示:
- 调用不确定性:不保证
finalize()方法会被及时执行,甚至不保证它会被执行。
- 性能开销:开启
finalize()会显著增加GC负担。
- 资源释放的误用:绝对不应依赖
finalize()来释放关键资源(如文件句柄、数据库连接)。
正确做法:使用try-finally块或try-with-resources语法(Java 7+)来确保资源被及时、确定地释放。
主流的垃圾回收器
HotSpot JVM提供了多种垃圾回收器,适用于不同场景。
1. Serial / Serial Old
单线程收集器,进行垃圾回收时必须暂停所有工作线程。简单高效,适用于客户端模式或资源受限环境。
2. ParNew
Serial收集器的多线程版本,主要与CMS收集器配合使用。
3. Parallel Scavenge / Parallel Old
JDK8的默认收集器,目标是达到一个可控制的吞吐量(用户代码运行时间 / (用户代码运行时间 + 垃圾回收时间))。适用于后台运算、对交互响应要求不高的场景。
4. CMS(Concurrent Mark Sweep)
以获得最短回收停顿时间为目标,大部分垃圾收集线程可与用户线程并发工作。适用于对延迟敏感的应用,如Web服务。
5. G1(Garbage-First)
面向服务端应用的收集器,将堆划分为多个大小相等的独立区域,能够建立可预测的停顿时间模型,同时兼顾高吞吐量。是JDK9及以后的默认GC。
6. ZGC / Shenandoah
新一代低延迟垃圾回收器,目标是在数TB级别的大堆上,将停顿时间控制在10毫秒以内。
实战:监控与调优GC
开启GC日志
监控是调优的第一步,通过JVM参数可以输出详细的GC日志。
-XX:+PrintGCDetails -Xloggc:/path/to/gc.log -XX:+PrintGCDateStamps
更现代的日志格式(JDK9+):
-Xlog:gc*:file=gc.log:time,tags:filecount=5,filesize=10M
内存泄漏排查示例
内存泄漏是导致OutOfMemoryError的常见原因。
class MemoryLeakDemo {
private static List<Object> eternalList = new ArrayList<>();
void createLeak() {
Object obj = new Object();
eternalList.add(obj); // 对象被静态集合强引用,永远无法回收
}
}
排查工具:可以使用JProfiler、VisualVM的堆转储分析功能或Eclipse MAT工具来分析堆内存快照,定位泄漏点。
基础调优参数
- 设置堆大小:
-Xms(初始堆大小)和-Xmx(最大堆大小)。生产环境通常设为相同值以避免堆动态调整带来的开销。例如:-Xms4g -Xmx4g。
- 设置新生代大小:
-Xmn 或通过比例 -XX:NewRatio。
- 选择GC器:例如,启用G1 GC:
-XX:+UseG1GC。
- 设置停顿时间目标(G1):
-XX:MaxGCPauseMillis=200。
GC最佳实践总结
-
代码层面:
- 及时断开不必要的对象引用(例如置为
null)。
- 谨慎使用静态集合,注意其生命周期。
- 对于大量临时小对象,考虑使用对象池(但需权衡GC与池化管理的开销)。
- 优先使用局部变量,让对象在方法调用结束后尽快失效。
-
JVM配置层面:
- 根据应用特性(吞吐量优先还是延迟敏感)和硬件资源选择最合适的GC器。
- 设置合理的堆大小,避免频繁的Full GC。
- 开启GC日志,并定期监控分析,作为调优的依据。这属于DevOps中应用性能监控的重要一环。
-
工具层面:
- 熟练使用
jstat、jmap、jstack等JDK命令行工具。
- 利用VisualVM、JMC(Java Mission Control)或第三方专业APM工具进行线上监控。
理解垃圾回收器的工作原理并进行有效的监控调优,是保证Java应用性能稳定、避免内存相关故障的关键技能。