上次的文章聊了記憶體回收行程的調優,當時囉嗦了比較長的篇幅,就沒再繼續提finalize的事兒(其實這玩意兒和GC是沾點兒邊的)。今天咱就把finalize函數相關的效能話題拿來說一下。
★finalize函數的調用機制
俺經常囉嗦瞭解本質機制的重要性。所以今天也得先談談finalize函數的調用機制。在聊之前,先聲明一下:Java虛擬機器規範(見“這裡”),並沒有硬性規定記憶體回收該不該搞,以及該如何搞。所以俺這裡提到的finalize函數的調用機制,或許適用於大多數JVM,但不保證能適用於所有的JVM。
◇何時被調用?
finalize啥時候才會被調用捏?一般來說,要等到JVM開始進行記憶體回收的時候,它才有可能被調用。而JVM進行記憶體回收的時間點是非常不確定的,依賴於各種運行時的環境因素。具體細節可以參見“本系列前一帖”。正是由於finalize函數調用時間點的不確定,導致了後面提到的某些缺點。
◇誰來調用?
說完何時調用,咱接著來聊一下被誰調用?
常見的JVM會通過GC的記憶體回收線程來進行finalize函數的調用。由於記憶體回收線程比較重要(人家好歹也是JVM的一個組成部分嘛),為了防止finalize函數拋出的異常影響到記憶體回收線程的運作,記憶體回收線程會在調用每一個finalize函數時進行trycatch,如果捕獲到異常,就直接丟棄,然後接著處理下一個失效對象的finalize函數。
★finalize函數的誤解和誤用
◇把finalize當成“解構函式”
學過C++的同學應該都知道“解構函式”(不懂C++的同學直接跳過此小節)。C++解構函式是在對象離開範圍的當口,立即被調用的。很多從C++轉Java的同學會想當然地把finalize函數牽強附會成C++的解構函式(兩者確實有某些相似之處)。然而,現實往往不是這麼美好滴。由於Java的finalize函數和C++的解構函式之間有許多非常顯著的差異,那些把finalize拿來當解構函式用的同學,註定是要碰壁滴(具體請看本文後面“finalize函數的缺點”)。
◇依靠finalize來釋放資源
很多同學寄希望於通過finalize()來完成類對象中某些資源的釋放(比如關閉資料庫連接之類)。有這種企圖的同學,請注意看本文後面的“finalize函數的缺點”!
★使用finalize函數的注意事項
下面介紹的注意事項,有些可能和效能最佳化關係不大,俺也一併列出來。
◇調用時間不確定——有資源浪費的風險
前面已經介紹了調用機制。同學們應該認清“finalize的調用時機是很不確定的”這樣一個事實。所以,假如你把某些稀缺資源放到finalize()中釋放,可能會導致該稀缺資源等上很久很久很久以後才被釋放。這可是資源的浪費啊!另外,某些類對象所攜帶的資源(比如某些JDBC的類)可能本身就很耗費記憶體,這些資源的延遲釋放會造成很大的效能問題。
◇可能不被調用——有資源泄漏的風險
很多同學以為finalize()總是會被調用,其實不然。在某些情況下,finalize()壓根兒不被調用。比如在JVM退出的當口,記憶體中那些對象的finalize函數可能就不會被調用了。
俺估摸著會有同學在打“runFinalizersOnExit”的主意,來確保所有的finalize在JVM退出前被調用。很可惜也很遺憾,該方法從JDK 1.2開始,就已經被廢棄了。即使該方法不被廢棄,也是有很大的安全執行緒隱患滴!企圖打這個主意的同學,趁早死了這條心吧。
從上述可以看出,一旦你依賴finalize()來幫你釋放資源,那可是很不妙啊(有資源泄漏的危險)!關於資源泄漏的嚴重性,俺在“這裡”曾經提到過。很多時候,資源流失導致的效能問題更加嚴重,萬萬不可小看。
◇對象可能在finalize函數調用時複活——有詐屍的風險
詐屍的情況比較少見,不過俺還是稍微提一下。
本來,只有當某個對象已經失效(沒有引用),記憶體回收行程才會調用該對象的finalize函數。但是,萬一碰上某個變態的程式員,在finalize()函數內部再把對象自身的引用(也就是this)重新儲存在某處,也就相當於把自己複活了(因為這個對象重新有了引用,不再處於失效狀態)。這種做法是不是夠變態啊
為了防止發生這種詭異的事情,記憶體回收行程只能在每次調用完finalize()之後再次去檢查該對象是否還處於失效狀態。這無形中又增加了JVM的開銷。
隨便提一下。由於JDK的文檔中規定了(具體見“這裡”),JVM對於每一個類對象執行個體最多隻會調用一次finalize()。所以,對於那些詐屍的執行個體,當它們真正死亡時,finalize()反而不會被調用了。這看起來是不是很奇怪?
◇要記得自己做異常捕獲
剛才在介紹finalize()調用機制時提到,一旦有異常拋出到finalize函數外面,會被記憶體回收線程捕獲並丟棄。也就是說,異常被忽略掉了(異常被忽略的危害,“這裡”有提到)。為了防止這種事兒,凡是finalize()中有可能拋出異常的代碼,你都得寫上try catch語句,自己進行捕獲。
◇要小心安全執行緒
由於調用finalize()的是記憶體回收線程,和你自己代碼的線程不是同一個線程;甚至不同對象的finalize()可能會被不同的記憶體回收線程調用(比如使用“並行收集器”的時候)。所以,當你在finalize()裡面訪問某些資料的時候,還得時刻留心安全執行緒的問題。
★結論
前面廢了這麼多話,最後稍微總結一下。俺竊以為:finalize實在是Java的雞肋。或許它對於極少數程式員有用,但對於大多數人(包括俺自個兒),這玩意兒沒啥明顯的好處。大伙兒還是盡量不用為妙。
著作權聲明
本部落格所有的原創文章,作者皆保留著作權。轉載必須包含本聲明,保持本文完整,並以超連結形式註明作者編程隨想和本文原始地址:
http://program-think.blogspot.com/2009/06/java-performance-tuning-4-finalize.html