finalize()是在java.lang.Object里定义的,也就是说每一个对象都有这么个方法。这个方法在gc启动,该对象被回收的时候被调用。其实gc可以回收大部分的对象(凡是new出来的对象,gc都能搞定,一般情况下我们不会用new以外的方式去创建对象),所以一般是不需要程序员去实现finalize的。
1、终结方法(finalizer)通常是不可预测的,也是很危险的,一般情况下是不必要的。是使用终结方法会导致行为不稳定、降低性能,以及可移植性问题。所以,我们应该避免使用终结方法。
2、使用终结方法有一个非常严重的性能损失。在我的机器上,创建和销毁一个简单对象的时间大约为5.6ns、增加一个终结方法使时间增加到了2400ns。换句话说,用终结方法创建和销毁对象慢了大约430倍。
3、如果实在要实现终结方法,要记得调用super.finalize()
上面的3点是出自Effective Java第二版第七条中的部分内容,可能刚开始我们看的时候一脸懵逼。有的人甚至都没听过finalize方法,更不知道用了它会出现什么问题了。下面我们来说说finalize方法。
protected void finalize() throws Throwable { }
我们可以看到Object中的finalize方法什么都没有实现,而且修饰符是protected,明显可以看出来是由子类去实现它的。这个方法的原意是在GC发生时销毁一些资源使用的,那么什么时候会调用这个方法呢?
原来在类加载的时候,会去检查一个类是否含有一个参数为空,返回值为void的finalize方法,还要求finalize方法必须非空。这个类我们暂时称为finalizer类(简称f类)。
比如我们有一个类A,它重写了finalize方法,在new A()的时候首先标记它是一个f类,然后调用Object的空构造方法,这个地方hotspot在初始化Object的时候将return指令替换为_return_register_finalizer指令,该指令并不是标准的字节码指令,是hotspot扩展的指令,这样在处理该指令时调用Finalizer.register方法,以很小的侵入性代价完美地解决了这个问题。下面是register的源码。
final class Finalizer extends FinalReference
通过源码我们可以知道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 discovered; /* used by VM */
static private class Lock { };
private static Lock lock = new Lock();
// 此属性保存一个PENDING的队列,配合上述next一起使用
private static Reference 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 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 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来操作的,下面是状态图:
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;
}
}
我们在回到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();
}
网上很多文章讲的很明白了:
Java的Finalizer引发的内存溢出
重载Finalize引发的内存泄露
主要原因是:Finalizer线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐。所以最后会发生OutOfMemoryError异常。
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/