學習effective java-7創建和銷燬對象之避免使用終結器(finalizers)

 

該知識點是自己從書籍中學習的筆記

背景

終結器的理念是允許 Java 方法釋放任何需要返回到操作系統的本機資源。使用Finalizers會帶來一些不可預期的危險、古怪的結果、性能降低、移植性問題,所以通常情況下,作爲一種規則,都不要使用Finalizers。當然Finalizers是有一點作用的。

在c++,通常是使用析構函數來回收已經分配給對象的資源或者其他資源。在java中,是自動回收不資源的,當然也可以通過try/finally代碼塊來實現和C++的析構函數的功能。下面將對使用Finalizers的優缺做說明。

不使用Finalizers的原因

1. Finalizers並不能夠保證立即執行,一個對象變爲不可獲取到finalizers執行的這段時間是不確定的。也就意味着,在finalizers爲執行的時候,你不能夠做任何事情。比如在finalizers中關閉文件的問題,如果finalizers未執行完畢的時候,再次打開文件的時候就會出現問題。

2. Finalizers不能夠立即執行,還可能導致內存溢出問題,因爲釋放的資源都需要在finalizers中執行,但是finalizers卻不能夠保證立即執行,這樣就導致內存積壓過多,程序就死掉了。

3. 不僅Java語言規範沒有提供Finalizers將被立即執行的保證,而且也沒有說Finalizers最終也會執行的保證。也就是說一些不可用的對象上的finalizer不一定會執行。因此,不要依靠finalizer來更新系統關鍵性的狀態。比如說,使用finalizer來釋放一個共享資源的永久鎖(數據庫)會導致你的分佈式系統垮掉。

4.System.gc 和 System.runFinalization雖然可以提高finalizer的執行機遇,但是並不能夠保證一定執行。System.runFinalizersOnExit和 Runtime.runFinalizersOnExit可以保證finalizer一定執行,但是這兩個方法有致命的危險,已經被捨棄了。

5.  正常情況一個未捕獲的異常都將被打印出來,但是如果將未捕獲的異常發生在finalizer的話,則什麼也不會打印出來。也就是說出了問題,我們都無法明白其原因。

6. 使用finalizers的話,會導致性能下降。

對一個類的對象封裝了要求中斷的資源,比如說fileds或者threads,替換finalizer的方法:在類中明確地提供一個
中斷的方法,當實例不再使用的時候,類的使用者對調用該方法。這種方法在:InputStream、OutputStream,java.sql.Connection
的close方法是很常見的;在java.util.Timer的cancel方法也這樣的;還有Graphics.dispose和Window.dispose。
明確地中斷方法經常是和try-finally配合着使用的。
// try-finally block guarantees execution of termination method
Foo foo = new Foo(...);
try {
// Do what must be done with foo
...
} finally {
foo.terminate(); // Explicit termination method
}

Finalizer帶來的好處:
1.充當“安全網”,當一個對象忘記調用明確地中斷方法的時候。使用這種方式總比不使用釋放資源要強。但是使用finalizer的時候一定要考慮是否要這樣做。同時需要在

finalizer中寫一個日誌記錄,方便調試。
2.對native peers對象來說,finalizer是有用的。因爲native object是不能夠被jvm回收的。

“Finalizer chaining”性能不是特別好,因此在子類中要通過super.finalizer()手動調用finalizer方法,並且要覆寫父類的finalizer方法:
// Manual finalizer chaining
@Override protected void finalize() throws Throwable {
try {
... // Finalize subclass state
} finally {
super.finalize();
}
}
如果子類覆寫了父類的finalizer方法,但是忘記了調用。它的父類的finalizer方法是不會被執行的。這種情況下,最好在類中聲明一個匿名類來終結資源。

額外知識

在一個類的finalize()方法中創建對象是很不好的,會造成意想不到的問題,如下:

public class Finalizers {

    static Finalizers finalizer;

    int value;

 

    public Finalizers(int value) {

       if (value < 0) {

           throw new IllegalArgumentException("Negative Finalizers value");

       }

       this.value = value;

    }

 

    public void finalize() {

       finalizer = this;

    }

 

    public static void main(String[] args) {

       try{

           Finalizers f = new Finalizers(-1);

       } catch (Exception e) {

       }

       System.gc();

        System.runFinalization();

 

       System.out.println(finalizer.value);

    }

}

執行結果是:0.

爲什麼會是0呢,Finalizers的構造方法明顯就做了檢查啊?這是因爲System.gc()System.runFinalization() 的調用促使 JVM 運行一個垃圾回收週期並運行一些終結器finalize(),就創建了一個包含無效值的 Finalizers對象。看到原因了吧。因此 ,不要在finalize中創建對象。

爲了更容易避免此類攻擊,而無需引入額外的代碼或限制,Java 設計人員修改了 JLS,聲明如果在構造 java.lang.Object 之前在構造函數中拋出了一個異常,該方法的 finalize() 方法將不會執行。

但是如何在構造 java.lang.Object 之前拋出異常呢?畢竟,任何構造函數中的第一行都必須是對 this() 或 super() 的調用。如果構造函數沒有包含這樣的顯式調用,將隱式添加對 super() 的調用。所以在創建對象之前,必須構造相同類或其超類的另一個對象。這最終導致了對 java.lang.Object 本身的構造,然後在執行所構造方法的任何代碼之前,構造所有子類。

要理解如何在構造 java.lang.Object 之前拋出異常,需要理解準確的對象構造順序。JLS 明確給出了這一順序。當創建對象時,JVM:

1.爲對象分配空間。

2.將對象中所有的實例變量設置爲它們的默認值。這包括對象超類中的實例變量。

3.分配對象的參數變量。

4.處理任何顯式或隱式構造函數調用(在構造函數中調用 this() 或 super())。

5.初始化類中的變量。

6.執行構造函數的剩餘部分。

重要的是構造函數的參數在處理構造函數內的任何代碼之前被處理。這意味着,如果在處理參數時執行驗證,可以通過拋出異常預防類被終結。

舉例如下:

class Invulnerable {

    int value = 0;

 

    public Invulnerable(int value) {

       this(checkValues(value));

       this.value = value;

    }

 

    private Invulnerable(Void checkValues) {

 

    }

 

    static Void checkValues(int value) {

       if (value < 0) {

           throw new IllegalArgumentException("Negative Finalizers value");

       }

       return null;

    }

 

    @Override

    public String toString() {

       return (Integer.toString(value));

    }

}

 

public class AttackInvulnerable extends Invulnerable {

    static Invulnerable vulnerable;

 

    public AttackInvulnerable(int value) {

       super(value);

    }

 

    public void finalize() {

       vulnerable = this;

    }

 

    public static void main(String[] args) {

       try {

           new AttackInvulnerable(-1);

       } catch (Exception e) {

           System.out.println(e);

       }

       System.gc();

       System.runFinalization();

       if (vulnerable != null) {

           System.out

                  .println("Invulnerable object " + vulnerable + "created!");

       } else {

           System.out.println("Attack failed");

       }

    }

}

Invulnerable 的公共構造函數調用一個私有構造函數,而後者調用 checkValues 方法來創建其參數。此方法在構造函數執行調用來構造其超類之前調用,該構造函數是 Object 的構造函數。所以如果 checkValues 中拋出了一個異常,那麼將不會終結 Invulnerable 對象。

 

輸出結果:

java.lang.IllegalArgumentException: Negative Finalizers value

Attack failed

 

總結

終結器是 Java 語言的一種不太幸運的功能。儘管垃圾收集器可自動回收 Java 對象不再使用的任何內存,但不存在回收本機內存、文件描述符或套接字等本機資源的機制。Java 提供了與這些本機資源交互的標準庫通常有一個 close() 方法,允許執行恰當的清理,但它們也使用了終結器來確保在對象錯誤關閉時,沒有資源泄漏。

對於其他對象,通常最好避免終結器。無法保證終結器將在何時運行,或者甚至它是否會運行。終結器的存在意味着在終結器運行之前,不會對無法訪問的對象執行垃圾收集,而且此對象可能使更多對象存活。這導致活動對象數量增加,進而導致 Java 對象的堆使用率增加。

終結器恢復即將被垃圾收集的能力無疑是終結機制工作方式的一種意外後果。較新的 JVM 實現現在保護代碼免遭此類安全隱患。

 

 

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