Effective Java Item7:Avoid Finalizers,解釋爲什麼finalize是不安全的,不建議使用

memory的釋放並不是通過finalize(),因爲finalize不安全而且影響能”。Effective Java一書中也提到:Avoid Finalizers。人都有潛在的叛逆意識,別人給的結論或者制定的規範,除非有足夠的理由說服你,除非懂得這麼做背後的原因,否則只能是死記硬背,沒有形象深入的理解,不能學到真正的東西。本文通過自己的理解和一些實際的例子,和大家一起更形象的理解finalize。還是那句經典的話“talking is cheap,show me the code”。


我們先看下TestObjectHasFinalize這個類提供了finalize方法

package finalize;

public class TestObjectHasFinalize
{
public static void main(String[] args)
{
while (true)
{
TestObjectHasFinalize heap = new TestObjectHasFinalize();
System.out.println("memory address=" + heap);
}
}

@Override
protected void finalize() throws Throwable
{
super.finalize();
System.out.println("finalize.");
}
}

運行這段程序,使用jmap命令查看對象佔用的內存情況。

C:\Documents and Settings\Administrator>jps
4232 Jps
3236 TestObjectHasFinalize
5272

C:\Documents and Settings\Administrator>jmap -histo:live 3236

num #instances #bytes class name
———————————————-
1: 106983 3423456 java.lang.ref.Finalizer
2: 106977 855816 finalize.TestObjectHasFinalize
3: 642 841384 [I
4: 5204 521984 <constMethodKlass>
5: 8678 460712 <symbolKlass>
6: 5204 460672 <methodKlass>
7: 1694 206832 [C
8: 351 206024 <constantPoolKlass>
9: 351 142864 <instanceKlassKlass>
10: 325 140040 <constantPoolCacheKlass>
11: 421 79648 [B
12: 1701 40824 java.lang.String
13: 420 40320 java.lang.Class
14: 519 33720 [S
15: 547 32800 [[I
16: 758 24256 java.util.TreeMap$Entry
17: 94 18024 <methodDataKlass>
18: 40 13120 <objArrayKlassKlass>
19: 312 12776 [Ljava.lang.Object;
20: 76 6080 java.lang.reflect.Method
21: 181 5840 [Ljava.lang.String;
22: 37 2664 java.lang.reflect.Field
23: 8 2624 <typeArrayKlassKlass>

可以發現佔用內存較多的是java.lang.ref.Finalizer對象和TestObjectHasFinalize,爲什麼會有這麼多個Finalizer對象呢?爲什麼要這麼多的TestObjectHasFinalize對象呢?類似的,我們看下沒有finalize()方法的情況

public class TestObjectNoFinalize
{
public static void main(String[] args)
{
while (true)
{
TestObjectNoFinalize heap = new TestObjectNoFinalize();
System.out.println("object no finalize method." + heap);
}
}
}

C:\Documents and Settings\Administrator>jps
4436 TestObjectNoFinalize
6012 Jps
5272

C:\Documents and Settings\Administrator>jmap -histo:live 4436

num #instances #bytes class name
———————————————-
1: 5203 521896 <constMethodKlass>
2: 8677 460696 <symbolKlass>
3: 5203 460584 <methodKlass>
4: 1689 206544 [C
5: 351 205992 <constantPoolKlass>
6: 351 142864 <instanceKlassKlass>
7: 325 140024 <constantPoolCacheKlass>
8: 421 79640 [B
9: 1696 40704 java.lang.String
10: 420 40320 java.lang.Class
11: 519 33720 [S
12: 547 32800 [[I
13: 758 24256 java.util.TreeMap$Entry
14: 407 22088 [I
15: 74 14720 <methodDataKlass>
16: 40 13120 <objArrayKlassKlass>
17: 312 12776 [Ljava.lang.Object;
18: 76 6080 java.lang.reflect.Method
19: 181 5840 [Ljava.lang.String;
20: 37 2664 java.lang.reflect.Field
21: 8 2624 <typeArrayKlassKlass>
22: 100 1976 [Ljava.lang.Class;
23: 20 1664 [Ljava.util.HashMap$Entry;
24: 12 1440 <klassKlass>
25: 59 1416 java.util.Hashtable$Entry
26: 13 728 java.net.URL
27: 18 720 java.util.HashMap
28: 7 680 [Ljava.util.Hashtable$Entry;
29: 6 672 java.lang.Thread
30: 10 640 java.lang.reflect.Constructor

可以發現,java.lang.ref.Finalizer和TestObjectHasFinalize沒有佔用大量的堆內存。沒有提供finalize()方法的類,佔用的堆內存更少,垃圾回收速度更快,而且JVM也不會創建那麼多java.lang.ref.Finalizer對象

1、使用finalize會導致嚴重的內存消耗和性能損失

《Effective Java》中提到“使用finalizer會導致嚴重的性能損失。在我的機器上,創建和銷燬一個簡單對象的實踐大約是5.6ns,增加finalizer後時間增加到2400ns。換言之,創建和銷燬帶有finalizer的對象會慢430倍”。額外的內存消耗這個很容看出,因爲使用了finalize的時候,堆內存中會多出很多java.lang.ref.Finalizer對象。性能損失這個可以通過分析得出結論,因爲使用了finalize的時候,堆內存會駐留大量的無用TestObjectHasFinalize對象。爲什麼有這麼多TestObjectHasFinalize對象呢?很簡單,垃圾回收的速度變慢了,對象的銷燬速度小於對象的創建速度。爲什麼有這麼多的java.lang.ref.Finalizer對象對象呢?這是JVM內部的機制,用來保證finalize只被調用一次。


2、JVM不確保finalize一定會被執行,而且執行finalize的時間也不確定。

從一個對象變爲不可達,到其finalizer被執行,可能會經過任意長時間。這意味着你不能在finalizer中執行任何注重時間的任務。依靠finalizer來關閉文件就是一個嚴重錯誤,因爲打開文件的描述符是一個有限資源。JVM會延遲執行finalizer,所以大量文件會被保持在打開狀態,當一個程序不再能打開文件的時候,就會運行失敗。

import sun.misc.Unsafe;

public class RevisedObjectInHeap
{
private long address = 0;

private Unsafe unsafe = GetUsafeInstance.getUnsafeInstance();

public RevisedObjectInHeap()
{
address = unsafe.allocateMemory(2 * 1024 * 1024);
}

@Override
protected void finalize() throws Throwable
{
super.finalize();
unsafe.freeMemory(address);
}

public static void main(String[] args)
{
while (true)
{
RevisedObjectInHeap heap = new RevisedObjectInHeap();
System.out.println("memory address=" + heap.address);
}
}

}

運行這段代碼,很快就會出現堆外內存溢出。爲什麼呢?就是因爲RevisedObjectInHeap.finalize方法不能及時執行,不能及時釋放堆外內存。可以參考我的另一篇博客:java中使用堆外內存,關於內存回收需要注意的事和沒有解決的遺留問題

當然使用finalize還有其他問題,具體的可以參考《Effective Java》。接下來介紹下JVM執行finalize方法的一些理論知識。實現了finalize()的對象,創建和回收的過程都更耗時。創建時,會新建一個額外的Finalizer 對象指向新創建的對象。 而回收時,至少需要經過兩次GC


先來看下新建對象的時候發生的事,測試步驟如下:

1、在TestObjectNoFinalize和TestObjectHasFinalize這2個類的while循環中打上斷點,在java.lang.ref.Finalizer的Finalizer()和register()打上斷點

2、分別debug運行TestObjectNoFinalize和TestObjectHasFinalize,觀察進入Finalizer斷點的次數

測試結果如下:

1、TestObjectNoFinalize沒有finalize()方法

      進入Finalizer斷點的次數很少(3次,指向的是JarFile、Inflater、FileInputStream),之後進入while循環中的斷點,不會再進入Finalizer中的斷點。也就是說:創建TestObjectNoFinalize對象的時候,不會創建相應的Finalizer對象。

2、TestObjectHasFinalize提供了finalize()方法

      每次創建TestObjectHasFinalize對象的時候,都會創建相應的Finalizer對象指向它。


再看下回收對象的時候發生的事:加上-XX:+PrintGCDetails參數,觀察下垃圾回收的過程。

可以看到TestObjectNoFinalize中都是在新生代中發生時的垃圾回收,很快就回收掉了內存。


TestObjectHasFinalize進行了幾次新生代內存回收之後,頻繁的進行[Full GC。這是以爲回收內存的速度太慢,導致新生代內存不能及時釋放,所以必須進行Full GC以期望獲取空閒的內存空間。這個實驗雖然不能直接證明至少需要進行2次GC,但是可以清楚的看到:含有finalize()的對象垃圾回收速度會很慢。


finalize機制的一些總結:

1、如果一個類A實現了finalize()方法,那麼每次創建A類對象的時候,都會多創建一個Finalizer對象(指向剛剛新建的對象);如果類沒有實現finalize()方法,那麼不會創建額外的Finalizer對象

2、Finalizer內部維護了一個unfinalized鏈表,每次創建的Finalizer對象都會插入到該鏈表中。源碼如下

// 存儲Finalizer對象的鏈表頭指針
static private Finalizer unfinalized = null;
static private Object lock = new Object();

private Finalizer next = null, prev = null;

private void add()
{
synchronized (lock)
{
if (unfinalized != null) {
this.next = unfinalized;
unfinalized.prev = this;
}
unfinalized = this;
}
}

3、如果類沒有實現finalize方法,那麼進行垃圾回收的時候,可以直接從堆內存中釋放該對象。這是速度最快,效率最高的方式

4、如果類實現了finalize方法,進行GC的時候,如果發現某個對象只被java.lang.ref.Finalizer對象引用,那麼會將該Finalizer對象加入到Finalizer類的引用隊列(F-Queue)中,並從unfinalized鏈表中刪除該結點。這個過程是JVM在GC的時候自動完成的。

/* Invoked by VM */
private void remove()
{
synchronized (lock)
{
if (unfinalized == this) {
if (this.next != null) {
unfinalized = this.next;
} else {
unfinalized = this.prev;
}
}
if (this.next != null) {
this.next.prev = this.prev;
}
if (this.prev != null) {
this.prev.next = this.next;
}
this.next = this; /* Indicates that this has been finalized */
this.prev = this;
}
}

// 這就是F-Queue隊列,存放的是Finalizer對象
static private ReferenceQueue queue = new ReferenceQueue();


5、含有finalize()的對象從內存中釋放,至少需要兩次GC。

第一次GC, 檢測到對象只有被Finalizer引用,將這個對象放入 java.lang.ref.Finalizer.ReferenceQueue 此時,因爲Finalizer的引用,對象還無法被GC。java.lang.ref.Finalizer$FinalizerThread
會不停的清理Queue的對象,remove掉當前元素,並執行對象的finalize方法。清理後對象沒有任何引用,在下一次GC被回收。

6、Finalizer是JVM內部的守護線程,優先級很低。Finalizer線程是個單一職責的線程。這個線程會不停的循環等待java.lang.ref.Finalizer.ReferenceQueue中的新增對象。一旦Finalizer線程發現隊列中出現了新的對象,它會彈出該對象,調用它的finalize()方法,將該引用從Finalizer類中移除,因此下次GC再執行的時候,這個Finalizer實例以及它引用的那個對象就可以回垃圾回收掉了。

private static class FinalizerThread extends Thread
{
FinalizerThread(ThreadGroup g) {
super(g, "Finalizer");
}
public void run() {
for (;;) {
try {
Finalizer f = (Finalizer)queue.remove();
f.runFinalizer();
} catch (InterruptedException x) {
continue;
}
}
}
}

7、使用finalize容易導致OOM,因爲如果創建對象的速度很快,那麼Finalizer線程的回收速度趕不上創建速度,就會導致內存垃圾越來越多

發佈了2 篇原創文章 · 獲贊 6 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章