finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。其实gc可以回收大部分的对象(凡是new出来的对象,gc都能搞定,一般情况下我们不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。
1、概述
1、终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。是使用终结方法会导致行为不稳定、降低性能,以及可移植性问题。所以,我们应该避免使用终结方法。
2、使用终结方法有一个非常严重的性能损失。在我的机器上,创建和销毁一个简单对象的时间大约为5.6ns、增加一个终结方法使时间增加到了2400ns。换句话说,用终结方法创建和销毁对象慢了大约430倍。
3、如果实在要实现终结方法,要记得调用super.finalize()
上面的3点是出自Effective Java第二版第七条中的部分内容,可能刚开始我们看的时候一脸懵逼。有的人甚至都没听过finalize方法,更不知道用了它会出现什么问题了。下面我们来说说finalize方法。
2、Object中的finalize方法
protected void finalize() throws Throwable { }
我们可以看到Object中的finalize方法什么都没有实现,而且修饰符是protected,明显可以看出来是由子类去实现它的。这个方法的原意是在GC发生时销毁一些资源使用的,那么什么时候会调用这个方法呢?
原来在类加载的时候,会去检查一个类是否含有一个参数为空,返回值为void的finalize方法,还要求finalize方法必须非空。这个类我们暂时称为finalizer类(简称f类)。
3、注册finalizer类
比如我们有一个类A,它重写了finalize方法,在new A()的时候首先标记它是一个f类,然后调用Object的空构造方法,这个地方hotspot在初始化Object的时候将return指令替换为_return_register_finalizer指令,该指令并不是标准的字节码指令,是hotspot扩展的指令,这样在处理该指令时调用Finalizer.register方法,以很小的侵入性代价完美地解决了这个问题。下面是register的源码。
final class Finalizer extends FinalReference<Object> {
// 引用队列
private static ReferenceQueue<Object> queue = new ReferenceQueue<>();
// 静态的Finalizer链
private static Finalizer unfinalized = null;
private static final Object lock = new Object();
private Finalizer
next = null,
prev = null;
private boolean hasBeenFinalized() {
return (next == this);
}
/**
* unfinalized链不为空,让自己指向unfinalized,unfinalized的prev指向自己
* unfinalized指向自己
* 最终unfinalized将指向最后加进来的对象,并且这个链包含所有实现finalize方法的对象
*/
private void add() {
synchronized (lock) {
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}
private Finalizer(Object finalizee) {
super(finalizee, queue);
add();
}
/* Invoked by VM */
// 这个register就是在new Object()的时候进行调用的
static void register(Object finalizee) {
new Finalizer(finalizee);
}
}
通过源码我们可以知道register除了把实现finalize方法的对象加到一个名为unfinalized的链表中外,还在构造方法中调用了super(finalizee, queue);,最终进入了Reference的构造方法中。
class FinalReference<T> extends Reference<T> {
public FinalReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
public abstract class Reference<T> {
// 用于保存对象的引用,GC会根据不同Reference来特别对待
private T referent; /* Treated specially by GC */
// 如果需要通知机制,则保存的对对应的队列
volatile ReferenceQueue<? super T> queue;
/* 这个用于实现一个单向循环链表,用以将保存需要由ReferenceHandler处理的引用 */
Reference next;
transient private Reference<T> discovered; /* used by VM */
static private class Lock { };
private static Lock lock = new Lock();
// 此属性保存一个PENDING的队列,配合上述next一起使用
private static Reference<Object> pending = null;
/* High-priority thread to enqueue pending References
*/
private static class ReferenceHandler extends Thread {
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
for (;;) {
Reference<Object> r;
synchronized (lock) {
if (pending != null) {
// 取得当前pending的Reference链
r = pending;
// pending指向Reference链的下一个元素discovered
pending = r.discovered;
r.discovered = null;
} else {
try {
try {
lock.wait();
} catch (OutOfMemoryError x) { }
} catch (InterruptedException x) { }
continue;
}
}
// Fast path for cleaners
if (r instanceof Cleaner) {
((Cleaner)r).clean();
continue;
}
ReferenceQueue<Object> q = r.queue;
// 入队列
if (q != ReferenceQueue.NULL) q.enqueue(r);
}
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
}
public T get() {
return this.referent;
}
public void clear() {
this.referent = null;
}
public boolean isEnqueued() {
return (this.queue == ReferenceQueue.ENQUEUED);
}
public boolean enqueue() {
return this.queue.enqueue(this);
}
Reference(T referent) {
this(referent, null);
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
}
Reference中有两个变量pending和discovered,我们它们两个没有地方可以赋值,都是由GC来操作的,下面是状态图:
Reference内部有一个线程ReferenceHandler,一旦使用了Reference,则会启动该线程。该线程会拿到pending的Reference,把它加入到ReferenceQueue中。并把queue的状态设为ENQUEUED,并通过Reference的next属性把对象串起来,犹如一个链表。下面是ReferenceQueue的enqueue()
boolean enqueue(Reference<? extends T> r) {
synchronized (lock) {
ReferenceQueue<?> queue = r.queue;
if ((queue == NULL) || (queue == ENQUEUED)) {
return false;
}
assert queue == this;
r.queue = ENQUEUED;
// r的next节点指向当前头结点
r.next = (head == null) ? r : head;
// 头结点指向当前对象r
head = r;
queueLength++;
if (r instanceof FinalReference) {
sun.misc.VM.addFinalRefCount(1);
}
lock.notifyAll();
return true;
}
}
4、调用finalize方法
我们在回到Finalizer类中,我们发现它里面也有一个内部线程,会先从queue中取出之前初始化对象时放进去的对象,在调用runFinalizer方法,这个方法主要就是调用对象的finalize方法,接着把对象置空,等待下一次gc清除对象。
private void runFinalizer(JavaLangAccess jla) {
synchronized (this) {
if (hasBeenFinalized()) return;
remove();
}
try {
Object finalizee = this.get();
if (finalizee != null && !(finalizee instanceof java.lang.Enum)) {
// 调用对象的finalize方法
jla.invokeFinalize(finalizee);
finalizee = null;
}
} catch (Throwable x) { }
super.clear();
}
private static class FinalizerThread extends Thread {
private volatile boolean running;
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
// ...
for (;;) {
try {
// 从queue中取出之前初始化放进去的元素
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer(jla);
} catch (InterruptedException x) {
// ignore and continue
}
}
}
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread finalizer = new FinalizerThread(tg);
finalizer.setPriority(Thread.MAX_PRIORITY - 2);
finalizer.setDaemon(true);
finalizer.start();
}
5、finalize方法导致内存溢出
网上很多文章讲的很明白了:
Java的Finalizer引发的内存溢出
重载Finalize引发的内存泄露
主要原因是:Finalizer线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐。所以最后会发生OutOfMemoryError异常。
6、结论
C++有析构函数这个东西,能够很好地在对象销毁前做一些释放外部资源的工作,但是java没有。Object.finalize()提供了与析构函数类似的机制,但是它不安全、会导致严重的内存消耗和性能降低,应该避免使用。best practice是:像java类库的IO流、数据库连接、socket一样,提供显示的资源释放接口,程序员使用完这些资源后,必须要显示释放。所以可以忘记Object.finalize()的存在。JVM启动的时候,会创建一个Finalizer线程来支持finalize方法的执行。关于引用和引用队列,java提供了4种引用类型,在垃圾回收的时候,都有自己各自的独特表现。ReferenceQueue是用来配合引用工作的,没有ReferenceQueue一样可以运行。创建引用的时候可以指定关联的队列,当GC释放对象内存的时候,会将引用加入到引用队列,这相当于是一种通知机制。当关联的引用队列中有数据的时候,意味着引用指向的堆内存中的对象被回收。通过这种方式,JVM允许我们在对象被销毁后,做一些我们自己想做的事情。JVM提供了一个ReferenceHandler线程,将引用加入到注册的引用队列中。
finalze机制是先执行Object.finalize()中的逻辑,后销毁堆中的对象;引用和队列机制,先销毁对象,后执行我们自己的逻辑。可以看到:使用引用和队列机制效率更高,因为垃圾对象释放的速度更快。如果是监控对象的销毁,那么最适合的是幽灵引用,如sun.misc.Cleaner就是使用幽灵引用,达到监控对象销毁的目的,NIO中使用的就是这个。
转载自:http://benjaminwhx.com/2018/01/28/%E9%81%BF%E5%85%8D%E4%BD%BF%E7%94%A8Finalize%E6%96%B9%E6%B3%95/