本文实践环境:
Operating System: macOS 14.1.1
Kernel: Darwin 23.1.0
Architecture: arm64Java version: 17.0.8
Java 面试中经常有这样一道题:
谈谈
final
,finally
和finalize
的区别。
这篇文章,笔者来简单梳理一下 Object
类中的 finalize
方法。
先说结论:Java 9 中已经将这个方法标记为废弃 @Deprecated(since="9")
, 在 Java 18 中又将其标记成了 @Deprecated(since="9", forRemoval=true)
,也就是说这个方法将在几个版本之后正式移除。既然是已经废弃和即将移除的特性,我们完全没有必要去关注它,知道有这个东西就行了。
forRemoval
是 Java 9 中引入的一个属性,默认为 false, 如果设置为 true,表示该废弃特性的删除已经提上日程。可能在两三个版本之后,就会将其删除。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
/**
* Returns the version in which the annotated element became deprecated.
* The version string is in the same format and namespace as the value of
* the {@code @since} javadoc tag. The default value is the empty
* string.
*
* @return the version string
* @since 9
*/
String since() default "";
/**
* Indicates whether the annotated element is subject to removal in a
* future version. The default value is {@code false}.
*
* @return whether the element is subject to removal
* @since 9
*/
boolean forRemoval() default false;
}
刚刚发布不久的 Java 21,这个方法还没有被删除,相信要不了多久,finalize
就要被干掉了。
作用
Java Doc 中,对 finalize
方法的描述就提到了它的作用:
Called by the garbage collector on an object when garbage collection determines that there are no more references to the object. A subclass overrides the finalize method to dispose of system resources or to perform other cleanup.
…
The finalize method of class Object performs no special action; it simply returns normally. Subclasses of Object may override this definition.
The Java programming language does not guarantee which thread will invoke the finalize method for any given object. It is guaranteed, however, that the thread that invokes finalize will not be holding any user-visible synchronization locks when finalize is invoked. If an uncaught exception is thrown by the finalize method, the exception is ignored and finalization of that object terminates.
…
The finalize method is never invoked more than once by a Java virtual machine for any given object.
Any exception thrown by the finalize method causes the finalization of this object to be halted, but is otherwise ignored.…
@Deprecated(since="9") protected void finalize() throws Throwable { }
简单来说,就是在对象被回收之前对某些外部资源进行清理,而调用者就是垃圾回收器。finalize
的本义是“最后决定,敲定;结束谈判,完成协议(或安排)”等,所以有的文章中将其翻译为“终结”。Java 要求,任何覆盖了 Object
类 finalize
方法的对象,在它不可达之后,回收之前必须执行它的 finalize
方法。
首先,我们通过一个简单的示例体验一下:
public class StudentEx extends Student {
private byte[] memory;
public StudentEx(String id, String name) {
super(id, name);
}
public StudentEx(String id, String name, byte[] memory) {
super(id, name);
this.memory = memory;
}
@Override
protected void finalize() throws Throwable {
try {
log.info("I am finalized: {}", getName());
// release some resources here...
} catch (Throwable throwable) {
log.error("finalize failed: ", throwable);
} finally {
super.finalize();
}
}
}
public class FinalizeUsageTest {
@SneakyThrows
@Test
void finalizeOnce() {
Student stu = new StudentEx("10001", "Jack");
stu = null;
log.info("Suggest the garbage collector to initiate a collection...");
System.gc();
TimeUnit.SECONDS.sleep(3);
}
}
输出日志:
08:26:43.971 [Test worker] INFO jvm.tech.demonlee.gc.finalize.FinalizeUsageTest - Suggest the garbage collector to initiate a collection...
08:26:43.983 [Finalizer] INFO jvm.tech.demonlee.common.model.StudentEx - I am finalized: Jack
从输出的日志中可以看到,执行 finalize
方法的线程名称为 Finalizer
,这是虚拟机自动创建的线程,优先级低,专门用来执行对象中重写的 Object#finalize
方法。需要说明的是,这里的执行,是指虚拟机会触发这个动作,但不一定会等它运行结束。因为,如果回收动作很慢或出现问题,可能会导致整个内存回收子系统出现崩溃。
在上面的 Java Doc 中还有这样一句话:
The finalize method is never invoked more than once by a Java virtual machine for any given object.
对于任何给定的对象,Java 虚拟机调用
finalize
方法不会超过一次。
什么意思?对象不是即将被垃圾回收吗,都要被杀掉了,怎么还有机会调用第二次?我们先通过一个例子,来看看内存到底有没有被回收掉:
public class FinalizeUsageTest {
// ...
// -Xlog:gc:time
@SneakyThrows
@Test
void finalizeMemory() {
Student stu = new StudentEx("10001", "Jack", new byte[1024 * 1024 * 200]);
stu = null;
log.info("Suggest the garbage collector to initiate a collection...");
System.gc();
TimeUnit.SECONDS.sleep(300);
log.info("test exit now...");
}
}
日志输出(为方便观察,笔者将 GC 日志和测试日志做了合并):
[2023-11-30T09:03:39.643+0800] Using G1
[2023-11-30T09:03:39.946+0800] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 25M->4M(512M) 2.384ms
09:03:40.152 [Test worker] INFO jvm.tech.demonlee.gc.finalize.FinalizeUsageTest - Suggest the garbage collector to initiate a collection...
[2023-11-30T09:03:40.161+0800] GC(1) Pause Full (System.gc()) 228M->207M(512M) 6.743ms
09:03:40.164 [Finalizer] INFO jvm.tech.demonlee.common.model.StudentEx - I am finalized: Jack
09:08:40.164 [Test worker] INFO jvm.tech.demonlee.gc.finalize.FinalizeUsageTest - test exit now...
从回收日志上看,触发 System.gc()
后,产生了一个 Full GC,但内存只释放 21 MB(228 - 207)。也就是说,一直到测试运行结束,这部分内存都没有及时释放,配合 jconsole
也可以观察到:
那什么时候释放?要不,我们再来一次 GC 试试?
public class FinalizeUsageTest {
// ...
// -Xlog:gc:time
@SneakyThrows
@Test
void finalizeMemoryWithGCAgain() {
Student stu = new StudentEx("10001", "Jack", new byte[1024 * 1024 * 200]);
stu = null;
log.info("Suggest the garbage collector to initiate a collection...");
System.gc();
TimeUnit.SECONDS.sleep(15);
log.info("Suggest the garbage collector to initiate a collection again...");
System.gc();
TimeUnit.SECONDS.sleep(15);
log.info("test exit now...");
}
}
输出日志如下,可以看到,在第二次 GC 中,这 200MB 内存被回收了,但 “I am finalized: Jack” 这句日志只打印了一次,也就说 finalize
方法确实只会调用一次。
[2023-11-30T09:50:47.028+0800] Using G1
[2023-11-30T09:50:47.317+0800] GC(0) Pause Young (Normal) (G1 Evacuation Pause) 25M->4M(512M) 2.220ms
09:50:47.514 [Test worker] INFO jvm.tech.demonlee.gc.finalize.FinalizeUsageTest - Suggest the garbage collector to initiate a collection...
[2023-11-30T09:50:47.522+0800] GC(1) Pause Full (System.gc()) 227M->207M(512M) 6.493ms
09:50:47.526 [Finalizer] INFO jvm.tech.demonlee.common.model.StudentEx - I am finalized: Jack
09:51:02.519 [Test worker] INFO jvm.tech.demonlee.gc.finalize.FinalizeUsageTest - Suggest the garbage collector to initiate a collection again...
[2023-11-30T09:51:02.536+0800] GC(2) Pause Full (System.gc()) 208M->6M(40M) 15.729ms
09:51:17.536 [Test worker] INFO jvm.tech.demonlee.gc.finalize.FinalizeUsageTest - test exit now...
李晓峰老师在《虚拟机设计与实现 (豆瓣)》一书中,有阐述 VM 对终结的处理逻辑,笔者简要整理如下:
- 当一个类被加载后,JVM 会检查它和它的父类是否重写了
Object
类中的finalize
方法,如果是,则给这个类打上finalizer
标记,即它有终结器; - 一个类被实例化成对象后,GC(垃圾回收器) 会检查这个类是否有
finalizer
标记,如果有,就将刚生产出来的对象实例加入到“终结器对象列表”,比如叫FinalizerQueue
; - 一次垃圾回收的过程中,当标记完成后,回收对象之前,GC 会遍历
FinalizerQueue
,检查对象的存活状态。如果对象已经判定为死亡,那么就将它从FinalizerQueue
中移除,加入到“可终结对象列表”中,比如叫FinalizableQueue
。至于存活的对象,依然还在FinalizerQueue
中; - GC 复活(没看错,是复活)
FinalizableQueue
中的死亡对象,将列表中每个对象及其可达对象都标记为活跃,然后把FinalizableQueue
传递给 JVM; - JVM 收到
FinalizableQueue
数据后,通常会使用专门的 finalizing 线程(一个或多个)来执行finalize
方法,也就是上面日志中我们看到的Finalizer
。不过在执行finalize
方法之前,该对象会从FinalizableQueue
移除。如果我们在finalize
方法中把对象自己赋值给一个静态变量呢?没错,对象又被拯救回来了,即该对象变得再次可达; - 当一个已终结对象再次变得不可达时,由于它已经不在
FinalizerQueue
中了,所以 GC 会直接回收它,也就不会有机会再次调用finalize
方法; - 最后,当 JVM 关闭时,会试图将所有对象的
finalize
方法执行完成。
需要补充说明的是,上面提到的 GC 复活,是指在这一个回收周期内,这些不可达对象能够得以保护,在堆中不会被 GC 回收掉。并且,这些从 FinalizerQueue
移入 FinalizableQueue
的对象,在应用程序中都是不可达的,即使有刚才提到的复活。当然,这些不可达对象之间会有依赖,所以相互之间可能是可达的。
如果换一种思路来思考,当我们重写 finalize
方法后,当前这次垃圾收集并没有回收对应的内存,这么做的目的可能是为了保证 finalize
方法能正常清除对应的特殊资源。
好,那我们就来看看如何用 finalize
拯救自己,这个例子来源于《深入理解Java虚拟机(第3版)》:
public class FinalizeEscapeGC {
private static FinalizeEscapeGC saveHook = null;
public void isAlive() {
log.info("Yes, I am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
log.info("oh, finalize method is executed.");
FinalizeEscapeGC.saveHook = this;
}
public static void main(String[] args) throws InterruptedException {
saveHook = new FinalizeEscapeGC();
testFinalize();
testFinalize();
}
private static void testFinalize() throws InterruptedException {
log.info("in testFinalize");
saveHook = null;
System.gc();
Thread.sleep(500);
if (Objects.nonNull(saveHook)) {
saveHook.isAlive();
} else {
log.info("No, I am dead :(");
}
}
}
输出日志:
17:33:51.817 [main] INFO jvm.tech.demonlee.gc.finalize.FinalizeEscapeGC - in testFinalize
17:33:51.824 [Finalizer] INFO jvm.tech.demonlee.gc.finalize.FinalizeEscapeGC - oh, finalize method is executed.
17:33:52.328 [main] INFO jvm.tech.demonlee.gc.finalize.FinalizeEscapeGC - Yes, I am still alive :)
17:33:52.329 [main] INFO jvm.tech.demonlee.gc.finalize.FinalizeEscapeGC - in testFinalize
17:33:52.838 [main] INFO jvm.tech.demonlee.gc.finalize.FinalizeEscapeGC - No, I am dead :(
从日志中可以看到,通过赋值给静态变量,我们成功将对象给拯救回来了。但前面已经提到 finalize
方法只会调用一次,所以当我们第二次发起 System.gc()
后,就没法再次拯救。
缺陷
说起 finalize
方法的由来,可能就不得不提 C++ 中的析构函数:
#include "iostream"
class DestructorTest {
private:
int *data;
public:
DestructorTest() {
data = new int[3];
for (int i = 0; i < 3; ++i) {
data[i] = i * 10;
}
cout << "Constructor called for memory allocate..." << endl;
}
~DestructorTest() {
delete[] data;
cout << "Destructor called for memory release...";
}
void print() {
for (int i = 0; i < 3; ++i) {
cout << "data element is: ";
cout << data[i] << endl;
}
}
};
void foo() {
DestructorTest demo;
demo.print();
}
int main() {
foo();
return 0;
}
输出日志:
Constructor called for memory allocate...
data element is: 0
data element is: 10
data element is: 20
Destructor called for memory release...
在 C++ 中,一个对象的析构函数总会在对象被销毁时调用,所以就有小伙伴将 Java 中的 finalize
方法看成 Java 版的析构函数。但很遗憾,它并不是,二者并不等同。可以这么理解:Java 中的 finalize
方法,更多的是 Java 刚诞生时做的一项妥协,目的则是为了让传统的 C/C++ 程序员更容易接受 Java。
我们知道,在 Java 中一切都是对象,所有的对象都能被垃圾收集器进行回收,那么这个垃圾收集器不就相当于 C++ 中的析构函数吗?是,也不是。垃圾收集器只负责一件事:回收不需要使用的内存。但肯定会有一些场景,不回收内存,而是回收其他资源的,比如文件描述符等。所以,垃圾收集器可以认为是析构函数功能的子集。
再举一个例子:一个用 C/C++ 编写的业务程序中,使用 malloc()
系列函数分配了内存,如果 Java 通过本地方法调用了该程序,那么当不需要这部分内存时,Java 必须使用某种处理方式来回收它,即显示调用 C/C++ 中的 free()
函数释放内存。
但使用 finalize
方法来处理能行吗?答案是否定的。笔者将 Java Doc 中的描述也摘录到了这里:
The finalization mechanism is inherently problematic. Finalization can lead to performance issues, deadlocks, and hangs. Errors in finalizers can lead to resource leaks; there is no way to cancel finalization if it is no longer necessary; and no ordering is specified among calls to finalize methods of different objects. Furthermore, there are no guarantees regarding the timing of finalization. The finalize method might be called on a finalizable object only after an indefinite delay, if at all. Classes whose instances hold non-heap resources should provide a method to enable explicit release of those resources, and they should also implement AutoCloseable if appropriate. The java.lang.ref.Cleaner and java.lang.ref.PhantomReference provide more flexible and efficient ways to release resources when an object becomes unreachable.
1)不一定被执行
Java 中并没有规定 finalize
方法执行的时间节点或时间期限。这是什么意思?因为垃圾对象不一定会被回收。
当内存资源充足时,虚拟机可能并不会立即发起内存回收,毕竟启动 GC 线程运行是需要与业务线程争抢资源的。如果极端一点,某些程序启动到运行结束,GC 可能都没有被执行过,当程序结束时,再把所用资源一次性归还给操作系统。所以,如果使用 finalize
方法来清理某些特定资源,也许这些代码根本不会被执行,或者比我们想象中的执行时间要延期很多,从而可能导致严重的内存泄露或其他 bug。
2)执行也不能保证正确
前面也提到,执行 finalize
方法的线程执行的结果如何,虚拟机是不关注的,如果关注,可能会拖累整个内存回收子系统。即使这样,如果有多个 finalizing
线程并发执行,或者单个 finalizing
线程与业务线程并发执行(具体看虚拟机如何去实现),都可能会带来意想不到的问题,比如执行顺序不正确导致的数据错乱。当多个 finalize
方法都需要某个锁时,或者这个锁被业务线程占用了,就可能会出现死锁。
3)执行可能会影响业务
如果需要执行 finalize
方法的对象很多,那么就会占用更多堆空间和处理器资源,这势必会影响到业务线程的正常运行,毕竟资源是有限的。如何让对象终结的速度与创建的速度取得平衡,可能并不容易,而且极易发生各种资源同步问题。
既然执行 finalize
方法的需求存在,但又不能让虚拟机在垃圾回收时处理,那我们该怎么做?
替代
其实 Java Docs 中已经给出了替代方案,首选的方式就是即用即关闭,比如 try-finally
或更好用的 try-with-resources
。下面是一个 try-with-resources
代码示例:
public class TryWithResourceTest implements AutoCloseable {
@Override
public void close() {
log.info("release some resources now...");
}
private void execute() throws InterruptedException {
log.info("do sth now...");
TimeUnit.SECONDS.sleep(1);
}
@Test
void usage() {
try (TryWithResourceTest test = new TryWithResourceTest()) {
test.execute();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
运行日志如下:
11:52:07.380 [Test worker] INFO jvm.tech.demonlee.tmp.TryWithResourceTest - do sth now...
11:52:08.388 [Test worker] INFO jvm.tech.demonlee.tmp.TryWithResourceTest - release some resources now...
第二种方式,则是利用 java.lang.ref.Cleaner
进行处理,它利用了幻象引用(关于引用,笔者将在下一篇文章中介绍)相关机制,下面是 JDK 官方提供的一个代码示例:
public class CleanerExample implements AutoCloseable {
// A cleaner, preferably one shared within a library
private static final Cleaner cleaner = Cleaner.create();
private final State state;
private final Cleaner.Cleanable cleanable;
public CleanerExample() {
this.state = new State();
this.cleanable = cleaner.register(this, state);
}
public void execute() {
log.info("do sth via state: {}", state);
}
@Override
public void close() {
cleanable.clean();
}
static class State implements Runnable {
State() {
// initialize State needed for cleaning action
log.info("create some resources here...");
}
@Override
public void run() {
// cleanup action accessing State, executed at most once
log.info("release some resources now...");
}
}
}
public class FinalizeResourceTest {
// 使用 try-with-resources 机制
@Test
void tryWithResourcesUsage() {
try (CleanerExample cleanerExample = new CleanerExample()) {
cleanerExample.execute();
}
}
// 假设忘记使用 try-with-resources,则利用 Cleaner 机制进行兜底
@Test
void cleanerUsage() throws InterruptedException {
CleanerExample cleanerExample = new CleanerExample();
cleanerExample.execute();
cleanerExample = null;
System.gc();
TimeUnit.SECONDS.sleep(1);
}
}
输出日志:
// tryWithResourcesUsage
14:24:04.141 [Test worker] INFO jvm.tech.demonlee.gc.finalize.CleanerExample - create some resources here...
14:24:04.146 [Test worker] INFO jvm.tech.demonlee.gc.finalize.CleanerExample - do sth via state: jvm.tech.demonlee.gc.finalize.CleanerExample$State@16f7b4af
14:24:04.146 [Test worker] INFO jvm.tech.demonlee.gc.finalize.CleanerExample - release some resources now...
// cleanerUsage
14:25:16.377 [Test worker] INFO jvm.tech.demonlee.gc.finalize.CleanerExample - create some resources here...
14:25:16.381 [Test worker] INFO jvm.tech.demonlee.gc.finalize.CleanerExample - do sth via state: jvm.tech.demonlee.gc.finalize.CleanerExample$State@16f7b4af
14:25:16.389 [Cleaner-0] INFO jvm.tech.demonlee.gc.finalize.CleanerExample - release some resources now...
这个例子中,同样实现了 AutoCloseable
接口,所以我们可以优先使用 try-with-resources
机制来清理资源,毕竟这种方式简单、直接、高效。当然,我们也可以利用 Cleaner
机制进行兜底,虚拟机也会启用独立的 Cleaner
进程来执行相关逻辑。
如果仔细观察就会发现,这个 Cleaner
机制正是 Java 9 引入的,正好与废弃 finalize
方法的时机相同。也就是说,官方废弃了一个旧特性,同时也给出了一个新的解决方案。不过使用 Cleaner
机制时也需要小心,因为它处理的机制是幻象引用,如果不能成为幻象引用呢?比如,将上面的 static class State
改成 class State
就不行了,因为一个非静态内部类是默认持有外部对象强引用的。也就是说,CleanerExample
无法变成幻象可达状态(Cleaner
引用 State
,State
又引用 CleanerExample
,而 Cleaner
又是静态变量,于是内存泄露就产生了),从而无法执行对应的清理操作。
如果想了解更多使用细节,请查阅相关资料,笔者在这里就不赘述了。但需要指出的是,如果同样出现终结对象堆积的情况,依然可能会出现各种问题,所以不要过多依赖 Cleaner
机制。
总结
最后简单小结一下,下面这段是摘录自 JEP 421:
Finalization has serious flaws that have been widely recognized for decades. Its presence in the Java Platform burdens the entire ecosystem because it exposes all library and application code to security, reliability, and performance risks. It also imposes ongoing maintenance and development costs on the JDK, particularly on the GC implementations. To move the Java Platform forward, we will deprecate finalization for removal.
既然官方都要废弃 finalize
,那只能说连 Java 的设计者们都没有完全理解这些问题,承认 finalize
的设计是一个错误。对于需要释放的资源,最好的办法就是即用即关闭,而不是依赖于底层的一个黑盒。有人做过 benchmark,实现了 finalize
方法的对象,在垃圾回收速度上呈现数量级的变慢,下降程度达到 40~50 倍。
所以,忘记它吧。