Java基礎四:避免使用finalize()方法
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中使用的就是這個。