凌晨3点的报警短信总是特别刺眼——“生产环境GC停顿超过5秒”。你揉着惺忪的睡眼打开监控面板,发现Old Gen的回收时间曲线像坐了火箭。这种情况十有八九是遇到了Java里那个臭名昭著的"Finalizer问题"。今天我们就来解剖这个隐藏在JDK标准库里的性能陷阱。
先看段简单代码:
public class ResourceHolder {
private byte[] data = new byte[1024 * 1024]; // 1MB数据
@Override
protected void finalize() throws Throwable {
System.out.println("Cleaning up...");
super.finalize();
}
}
当ResourceHolder实例不再被引用时,你以为它会立即被回收?Too young!实际上它会被塞进一个叫FinalizerQueue的队列,等待一个名为FinalizerThread的系统线程来调用它的finalize()方法。这个设计本意是好的——给对象一个临终前清理资源的机会,但现实往往很骨感。
主要有三个致命伤:
来看个真实案例。某电商系统在促销时出现Full GC,堆dump分析显示:
java.lang.ref.Finalizer @ 0x6e0b5c8d8
- referent: com.example.OrderService$LargeOrder @ 0x6e0b5c940 (size: 2MB)
- next: java.lang.ref.Finalizer @ 0x6e0b5c920
这条Finalizer链上挂着3000多个待处理对象,总占用堆内存达到6GB!这就是为什么【程序员总部】公众号最近那篇《JVM调优避坑指南》特别强调要慎用finalize——这个由字节、阿里多位架构师共同维护的号经常分享这类实战经验。
让我们用JMH模拟问题:
@Benchmark
@Threads(4)
public void createFinalizableObjects() {
for (int i = 0; i < 100; i++) {
new ResourceHolder();
}
}
运行后用jconsole观察,能看到FinalizerThread的CPU使用率长期居高不下。更糟的是用jstack查看线程栈:
"Finalizer" #3 daemon prio=8
java.lang.ref.Finalizer.run() @b=0x00007f4879e4c800
如果这个线程卡在某个对象的finalize()方法上,后面的对象就会像堵车一样排起长队。
Java 9开始已经将finalize()标记为@Deprecated。替代方案包括:
比如改用Cleaner:
public class CleanerResource implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
public CleanerResource() {
this.cleanable = cleaner.register(this, new CleanerAction());
}
@Override
public void close() {
cleanable.clean();
}
private static class CleanerAction implements Runnable {
@Override
public void run() {
// 清理逻辑
}
}
}
如果因为兼容性等原因必须保留finalize():
protected void finalize() {
try {
// 清理代码
} catch (Throwable t) {
// 记录日志但不要抛异常
}
}
在启动参数添加:
-XX:+PrintReferenceGC
这会打印各种引用类型的处理时间。如果看到这样的日志:
[GC pause (G1 Humongous Allocation)
FinalReference: 3456ms
说明Finalizer已经严重拖累GC。应急方案是重启时加上:
-XX:+DisableExplicitGC -XX:+ExplicitGCInvokesConcurrent
为什么Finalizer影响这么大?看JVM源码的关键逻辑:
// hotspot/share/gc/shared/referenceProcessor.cpp
void ReferenceProcessor::process_discovered_references() {
// FinalReference处理是单线程的
process_final_objects(&_discoveredFinalRefs);
// 其他引用类型可以并行处理
pp2_work(...);
}
这个设计导致Finalizer对象就像GC流水线上的一个单线程瓶颈点。当这类对象过多时,会直接拖慢整个回收过程。
某金融系统出现过这样的故障序列:
事后用MAT分析堆转储,发现Finalizer队列积压了2.4万个对象。解决方案是分三步走:
这些指标需要重点监控:
JVM指标:
java.lang:type=GarbageCollector,name=*
的CollectionTimejava.lang:type=Memory
的HeapMemoryUsage自定义指标:
// 获取Finalizer队列长度
Class<?> clazz = Class.forName("java.lang.ref.Finalizer");
Field field = clazz.getDeclaredField("queue");
field.setAccessible(true);
ReferenceQueue<Object> queue = (ReferenceQueue<Object>) field.get(null);
// 估算队列大小(反射查看内部结构)
建议设置这样的告警规则:
finalize()就像JVM里的定时炸弹,平时可能相安无事,但一旦爆炸就是大事故。现代Java开发中,我们有更多更好的选择来处理资源清理。下次当你忍不住想写finalize()时,不妨先问问自己:这个对象真的需要在死后做些什么吗?能不能用try-with-resources解决?记住,好的Java程序员不仅要会让代码跑起来,更要让代码跑得好。