面试考点:为什么不推荐使用?有什么替代方案?
警告:你即将了解Java中最"坑"的特性之一!⚠️
finalize()方法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Java设计者:"我们创造了一个优雅的特性..."
程序员:"太好了!"
5年后...
Java设计者:"呃...别用它..." 😅
程序员:"???" 🤔
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🎯 什么是finalize()?
定义 📖
public class Object {
/**
* 当对象即将被GC回收时调用
* @deprecated JDK 9后标记为废弃
*/
@Deprecated(since="9")
protected void finalize() throws Throwable {
// 默认什么都不做
}
}
设计初衷 💡
设计者的美好愿景:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
"对象被回收前,可以做最后的清理工作!"
比如:
- 关闭文件
- 释放网络连接
- 释放本地资源
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
理想很丰满,现实很骨感... 😭
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
生活类比 🏠
finalize() = 遗嘱 📜
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
理想:
- 人去世后,遗嘱执行
- 安排后事
现实:
- 不知道什么时候执行
- 可能永远不执行
- 执行了可能出错
- 还可能"诈尸"(对象复活)💀
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💀 finalize的七宗罪
罪状1: 不确定性 ⏰
问题:不知道何时执行,甚至可能不执行!
public class UncertaintyDemo {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize被调用了!");
super.finalize();
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new UncertaintyDemo();
}
System.gc(); // 请求GC
// 等待一会...
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
// 可能输出:
// finalize被调用了!
// finalize被调用了!
// (只调用了2次?其他8个呢?😱)
}
}
原因:
GC何时执行? → 不确定 ⏰
finalize何时被调用? → 不确定 ⏰
finalize是否会被调用? → 不确定 ⏰
罪状2: 性能杀手 🐌
问题:严重影响GC性能!
// 性能测试
public class PerformanceTest {
// 测试1: 普通对象
public static void testNormal() {
long start = System.currentTimeMillis();
for (int i = 0; i < 10_000_000; i++) {
new Object();
}
System.gc();
long end = System.currentTimeMillis();
System.out.println("普通对象: " + (end - start) + "ms");
}
// 测试2: 有finalize的对象
static class FinalizeObject {
@Override
protected void finalize() throws Throwable {
super.finalize();
}
}
public static void testFinalize() {
long start = System.currentTimeMillis();
for (int i = 0; i < 10_000_000; i++) {
new FinalizeObject();
}
System.gc();
long end = System.currentTimeMillis();
System.out.println("finalize对象: " + (end - start) + "ms");
}
public static void main(String[] args) {
testNormal(); // 输出: 500ms
testFinalize(); // 输出: 40000ms 😱 慢80倍!
}
}
为什么这么慢?
普通对象GC流程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 标记
2. 清理
✅ 完成!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
有finalize的对象:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 标记
2. 判断需要finalize
3. 放入F-Queue队列 ← 多一步!
4. Finalizer线程执行finalize ← 多一步!
5. 再次GC才能真正回收 ← 又多一步!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
对象至少要经历两次GC才能被回收!💀
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
罪状3: 对象复活 🧟
问题:finalize中可以"复活"对象!
public class ZombieObject {
public static ZombieObject instance = null;
@Override
protected void finalize() throws Throwable {
System.out.println("finalize被调用,我要复活!");
// 复活自己!
instance = this; // 💀 诈尸了!
super.finalize();
}
public static void main(String[] args) throws Exception {
instance = new ZombieObject();
System.out.println("第一次: " + instance);
// 第一次"杀死"对象
instance = null;
System.gc();
Thread.sleep(1000);
System.out.println("第二次: " + instance); // 💀 复活了!
// 第二次"杀死"对象
instance = null;
System.gc();
Thread.sleep(1000);
System.out.println("第三次: " + instance); // null(真死了)
}
}
// 输出:
// 第一次: ZombieObject@123
// finalize被调用,我要复活!
// 第二次: ZombieObject@123 ← 复活了!
// 第三次: null ← finalize只执行一次,这次真死了
原理:
finalize()只会被调用一次!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一次GC:
- 调用finalize()
- 对象复活
- 标记"finalize已执行"
第二次GC:
- 检查"finalize已执行"
- 不再调用finalize()
- 直接回收
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
罪状4: 异常被吞掉 🤐
问题:finalize中的异常会被忽略!
public class ExceptionDemo {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize开始");
throw new RuntimeException("出错了!");
// 这个异常会被吞掉,不会抛出!😱
}
public static void main(String[] args) {
new ExceptionDemo();
System.gc();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {}
System.out.println("程序继续运行...");
// 输出:
// finalize开始
// 程序继续运行...
// (异常没了?😱)
}
}
源码分析:
// Finalizer.java(JDK内部)
private void runFinalizer(JavaLangAccess jla) {
try {
// 调用finalize
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
jla.invokeFinalize(finalizee);
}
} catch (Throwable x) {
// 异常被吞掉了!😱
}
}
罪状5: 执行顺序不保证 🎲
问题:多个对象的finalize执行顺序是随机的!
public class OrderDemo {
private String name;
OrderDemo(String name) {
this.name = name;
}
@Override
protected void finalize() throws Throwable {
System.out.println(name + " 被回收");
super.finalize();
}
public static void main(String[] args) throws Exception {
new OrderDemo("A");
new OrderDemo("B");
new OrderDemo("C");
System.gc();
Thread.sleep(1000);
// 可能输出:
// C 被回收
// A 被回收
// B 被回收
// (顺序完全随机!😱)
}
}
罪状6: 单线程执行 🐌
问题:所有finalize由一个线程执行!
// Finalizer线程(JDK内部)
private static class FinalizerThread extends Thread {
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer"); // ← 只有一个线程!
}
public void run() {
for (;;) {
try {
Finalizer f = (Finalizer) queue.remove();
f.runFinalizer(jla); // 顺序执行
} catch (InterruptedException x) {
continue;
}
}
}
}
后果:
如果一个finalize执行很慢:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Thread.sleep(10000); // 慢finalize
结果:
- 后续所有finalize都要等待!
- F-Queue堆积
- 内存泄漏!💥
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
罪状7: 安全漏洞 🔓
问题:可以通过finalize绕过构造器安全检查!
public class SecurityDemo {
public SecurityDemo() {
// 安全检查
if (!isAuthorized()) {
throw new SecurityException("未授权!");
}
}
private boolean isAuthorized() {
return false; // 假设未授权
}
public void sensitiveOperation() {
System.out.println("执行敏感操作!");
}
}
// 攻击代码
public class Attack extends SecurityDemo {
public static Attack instance = null;
public Attack() {
super(); // 会抛出SecurityException
}
@Override
protected void finalize() throws Throwable {
// 对象"复活"
instance = this;
super.finalize();
}
public static void main(String[] args) throws Exception {
try {
new Attack();
} catch (Exception e) {
// 构造失败,但对象已创建
}
System.gc();
Thread.sleep(1000);
if (instance != null) {
instance.sensitiveOperation(); // 😱 绕过了安全检查!
}
}
}
修复:
// 使用final防止子类化
public final class SecurityDemo {
// ...
}
// 或在finalize中再次检查
@Override
protected final void finalize() throws Throwable {
// 什么都不做,防止子类覆盖
}
🛡️ 替代方案
方案1: try-with-resources(推荐)✅
// ❌ 使用finalize
public class FileHandler {
private FileInputStream fis;
public FileHandler(String path) throws IOException {
fis = new FileInputStream(path);
}
@Override
protected void finalize() throws Throwable {
if (fis != null) {
fis.close(); // 何时执行?不确定!
}
super.finalize();
}
}
// ✅ 使用try-with-resources
public class FileHandler implements AutoCloseable {
private FileInputStream fis;
public FileHandler(String path) throws IOException {
fis = new FileInputStream(path);
}
@Override
public void close() throws IOException {
if (fis != null) {
fis.close(); // 确定在try块结束时执行!
}
}
// 使用
public static void main(String[] args) {
try (FileHandler handler = new FileHandler("test.txt")) {
// 使用handler...
} // 自动调用close()!✅
}
}
方案2: Cleaner API(JDK 9+)🧹
import java.lang.ref.Cleaner;
public class ResourceHolder {
// 创建Cleaner
private static final Cleaner cleaner = Cleaner.create();
// 资源状态(必须是静态内部类!)
private static class State implements Runnable {
private long nativeResource;
State(long nativeResource) {
this.nativeResource = nativeResource;
}
@Override
public void run() {
// 清理本地资源
if (nativeResource != 0) {
releaseNativeResource(nativeResource);
nativeResource = 0;
}
}
private static native void releaseNativeResource(long ptr);
}
private final State state;
private final Cleaner.Cleanable cleanable;
public ResourceHolder(long nativeResource) {
this.state = new State(nativeResource);
// 注册清理动作
this.cleanable = cleaner.register(this, state);
}
// 手动清理(推荐)
public void close() {
cleanable.clean();
}
}
使用Cleaner的要点:
✅ State必须是静态类(否则持有外部对象引用)
✅ State不能引用ResourceHolder实例
✅ 提供手动close()方法(Cleaner只是保底)
✅ Cleaner比finalize好,但仍不如try-with-resources
方案3: PhantomReference + ReferenceQueue 👻
public class ResourceManager {
private static final ReferenceQueue<Resource> queue = new ReferenceQueue<>();
private static final Set<ResourcePhantomReference> refs = new HashSet<>();
// 启动清理线程
static {
Thread cleanupThread = new Thread(() -> {
while (true) {
try {
ResourcePhantomReference ref = (ResourcePhantomReference) queue.remove();
ref.cleanup();
refs.remove(ref);
} catch (InterruptedException e) {
break;
}
}
});
cleanupThread.setDaemon(true);
cleanupThread.start();
}
static class ResourcePhantomReference extends PhantomReference<Resource> {
private final long nativePtr;
ResourcePhantomReference(Resource resource, long nativePtr) {
super(resource, queue);
this.nativePtr = nativePtr;
refs.add(this);
}
void cleanup() {
// 清理本地资源
releaseNative(nativePtr);
}
private static native void releaseNative(long ptr);
}
public static Resource createResource(long nativePtr) {
Resource resource = new Resource();
new ResourcePhantomReference(resource, nativePtr);
return resource;
}
}
方案4: Shutdown Hook 🪝
// 适用于JVM关闭时的清理
public class ShutdownHookDemo {
public static void main(String[] args) {
// 注册关闭钩子
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("执行清理工作...");
// 关闭数据库连接
// 刷新缓存
// 释放资源
}));
System.out.println("程序运行中...");
// JVM关闭时,自动执行钩子
}
}
📊 方案对比
| 方案 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|
| finalize | 无(已废弃) | 性能差、不确定、危险 | ❌ 不要用! |
| try-with-resources | 确定性、简单、高效 | 需要显式try块 | ⭐⭐⭐⭐⭐ |
| Cleaner | 安全、比finalize好 | 不确定性、复杂 | ⭐⭐⭐ |
| PhantomReference | 灵活、可控 | 复杂、需要线程 | ⭐⭐⭐ |
| Shutdown Hook | JVM关闭时执行 | 只在JVM关闭时 | ⭐⭐⭐⭐ |
🎯 最佳实践
实践1: 永远不要依赖finalize 🚫
// ❌ 错误
public class BadExample {
@Override
protected void finalize() throws Throwable {
releaseResource(); // 不确定何时执行!
super.finalize();
}
}
// ✅ 正确
public class GoodExample implements AutoCloseable {
@Override
public void close() {
releaseResource(); // 确定执行!
}
}
实践2: 防御性编程 🛡️
public class DefensiveResource implements AutoCloseable {
private volatile boolean closed = false;
@Override
public void close() {
if (!closed) {
closed = true;
releaseResource();
}
}
// Cleaner作为保底(但不依赖它!)
private final Cleaner.Cleanable cleanable;
public DefensiveResource() {
State state = new State();
this.cleanable = cleaner.register(this, state);
}
private static class State implements Runnable {
@Override
public void run() {
// 如果忘记close,Cleaner会执行
// 但这是最后的防线!
}
}
}
实践3: 文档化清理职责 📄
/**
* 数据库连接
*
* <p><b>资源管理:</b>
* 使用完毕后必须调用 {@link #close()} 方法释放连接。
* 推荐使用 try-with-resources:
* <pre>{@code
* try (Connection conn = getConnection()) {
* // 使用连接...
* } // 自动关闭
* }</pre>
*
* @author Your Name
*/
public class Connection implements AutoCloseable {
@Override
public void close() {
// 释放连接
}
}
💡 Pro Tips
Tip 1: 检测finalize使用 🔍
# 使用JDK工具检测
jdeps --jdk-internals myapp.jar | grep finalize
# 使用IDE
# IntelliJ IDEA: Analyze → Run Inspection by Name → "finalize"
Tip 2: 监控F-Queue 📊
// 监控finalize队列长度
ThreadMXBean threadMBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] infos = threadMBean.dumpAllThreads(false, false);
for (ThreadInfo info : infos) {
if ("Finalizer".equals(info.getThreadName())) {
System.out.println("Finalizer线程状态: " + info.getThreadState());
// 如果是WAITING,可能队列堆积了!
}
}
Tip 3: 迁移代码 🔄
// 替换finalize的模板
public class Migration implements AutoCloseable {
// 1. 实现AutoCloseable
@Override
public void close() {
// 2. 将finalize中的清理代码移到这里
cleanup();
}
private void cleanup() {
// 原来的清理逻辑
}
// 3. 删除finalize方法
// @Override
// protected void finalize() throws Throwable { ... }
// 4. 添加Cleaner作为保底(可选)
private final Cleaner.Cleanable cleanable =
cleaner.register(this, () -> cleanup());
}
🎓 面试要点
高频问题
Q1: 为什么不推荐使用finalize?
答案:
- 性能问题:对象回收变慢,至少需要两次GC
- 不确定性:不知道何时执行,甚至可能不执行
- 安全问题:可以通过finalize复活对象,绕过安全检查
- 异常吞掉:finalize中的异常会被忽略
- 单线程执行:所有finalize顺序执行,容易阻塞
Q2: finalize的替代方案是什么?
答案:
- 首选:try-with-resources + AutoCloseable
- 次选:Cleaner API(JDK 9+)
- 特殊场景:Shutdown Hook、PhantomReference
Q3: finalize只执行一次的原理?
答案: 对象创建时,标记为"未finalize":
- 第一次GC:调用finalize(),标记为"已finalize"
- 后续GC:检查"已finalize",不再调用
- 如果finalize中复活对象,下次GC直接回收
🎉 总结
🎯 核心观点
finalize() = Java历史的遗留问题
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ JDK 9标记为@Deprecated
✅ 未来版本可能移除
✅ 绝对不要在新代码中使用!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 迁移清单
- ☑️ 审查代码中的finalize使用
- ☑️ 替换为try-with-resources
- ☑️ 实现AutoCloseable接口
- ☑️ 添加Cleaner作为保底(可选)
- ☑️ 更新文档和注释
- ☑️ 测试资源释放
记住:finalize是Java的"黑历史",了解它是为了不犯同样的错误!💣
🌟 "好的程序员不仅要知道怎么做,更要知道什么不该做!" 😎
🎊 恭喜!你已经完成了21-30知识点的学习!
这些文档涵盖了JVM的核心知识:
- 📦 Class文件结构
- 💥 错误类型辨析
- ⚡ 启动优化
- 🔥 低延迟GC
- 🎯 分代理论
- 📊 GC调优
- 🎛️ 监控工具
- 👻 引用类型
- 🔐 锁优化
- 💣 finalize陷阱
你已经掌握了Java高级工程师的必备技能! 🏆