final、finally、finalize有什麼不同
final可以用來修飾類、方法、變數,分別有不同的意義,final修飾的類代表不可以繼承擴充套件,final的變數是不可以修改的,而final的方法也是不可以重寫的。
finally則是java保證重點程式碼一定要被執行的一種機制。我們可以用try-finally或者try-catch-finally來進行類似關閉JDBC連線、保證unlock鎖等動作。
finalize是基礎類java.lang.Object的一個方法,它的設計目的是通過物件在被垃圾收集前完成特定資源的回收。finalize機制現在已經不推薦使用,並且在JDK9開始被標記為deprecated。
推薦使用final關機子來明確表示我們程式碼的語意、邏輯意圖,這已經被證明在很多場景下是非常好的實踐,比如:
(1)我們可以將方法或類宣告為final,這樣就可以告知別人,這些行為是不許修改的。
如果你關注過java核心類庫的定義或原始碼,有沒有發現java.lang包下面的很多類,相當一部分都被宣告為final class?在第三方類庫的一些基礎類中同樣如此,這可以有效避免API使用者更改基礎功能,某種程度上,這是保證平臺安全的必要手段。
(2)使用final修飾引數或者變數,也可以清楚地避免意外賦值導致的程式設計錯誤,甚至,有人明確推薦將所有的方法引數、本地變數、成員變數宣告成final。
(3)final變數產生了某種程度的不可變(immutable)的效果,所以,可以用於保護只讀資料,尤其是在併發程式設計中,因為明確地不能再賦值final變數,有利於減少額外的同步開銷,也可以省去一些防禦性拷貝的必要。
final也許會有效能的好處,很多文章或者書籍中都介紹了可在特定場景提高效能,比如,利用final可能有助於JVM將方法進行內聯,可以改善編譯器進行條件編譯的能力等等。坦白說,很多類似的結論都是基於假設得出的,比如現代高效能JVM(如HotSpot)判斷內聯未必依賴final的提示,要相信JVM還是非常智慧的。類似的,final欄位對效能的影響,大部分情況下,並沒有考慮的必要。
從開發實踐的角度,我不想過度強調這一點,這是和JVM的實現相關的,未經驗證比較難以把握。我的建議是,在日常開發中,除非有特別考慮,不然最好不要指望這種小技巧帶來的所謂效能好處,程式最好是體現它的語意目的。如果你確實對這方面感興趣,可以查閱相關資料,我就不再贅述了,不過千萬別忘了驗證一下。
對於finally,明確知道怎麼使用就足夠了。需要關閉的連線等資源,更推薦使用java7中新增的try-with-resources語句,因為通常java平臺能夠更好地處理異常情況,編碼量也要少很多。
對於,finalize,如果沒有特別的原因,不要實現finalize方法,也不要指望利用它來進行資源回收。為什麼呢?簡單說,你無法保證finalize什麼時候執行,執行的是否符合預期。使用不當會影響效能,導致程式死鎖、掛起等。
通常來說,利用上面提到的try-with-resources或者try-finally機制,是非常好的回收資源的辦法。如果確實需要額外處理,可以考慮java提供的Cleaner機制或者其他替代方法。
這裡有幾點需要注意的地方:
一、注意,final不是immutable
我在前面介紹了final在實踐中的益處,需要注意的是,final並不等同於immutable,比如下面這段程式碼:
final List<String> strList = new ArrayList<>();
strList.add("Hello");
strList.add("world");
List<String> unmodifiableStrList = List.of("hello", "world");
unmodifiableStrList .add("again");
final智慧約束strList這個引用不可以被賦值,但是strList物件行為不被final影響,新增元素等操作是完全正常的。如果我們真的希望物件本身是不可變的,那麼需要相應的類支援不可變的行為。在上面這個例子中,List.of 方法建立的本身就是不可變List,最好那句add是會在執行時丟擲異常的。
Immutable在很多場景是非常棒的選擇,某種意義上說,java語言目前並沒有原生的不可變支援,如果要實現immutable的類,我們需要做到:
(1)將class自身宣告為final,這樣別人就不能擴充套件來繞過限制了。
(2)將所有成員變數定義為private和final,並且不要實現setter方法。
(3)通常構造物件時,成員變數使用深度拷貝來初始化,而不是直接賦值,這是一種防禦措施,因為你無法確定輸入物件不會 被其他人修改。
(4)如果確實需要實現getter方法,或者其他可能會返回內部狀態的方法,使用copy-on-write原則,建立私有的copy。
這些原則是不是在併發程式設計實踐中經常被提到?的確如此。關於setter/getter方法,很多人喜歡直接用IDE一次全部生成,建議最好是你確定有需要時再實現。
二、finalize真的那麼不堪?
前面簡單介紹了finalize是一種已經被業界證明了的非常不好的實踐,那麼為什麼會導致那些問題呢?
finalize的執行是和垃圾收集關聯在一起的,一旦實現了非空的finalize方法,就會導致相應物件回收呈現數量級上的變慢,有人專門做過benchmark,大概是40~50倍的下降。
這是因為,finalize倍設計成在物件唄垃圾收集前呼叫,這就意味著實現了finalize方法的物件是個“特殊公民”,JVM要對它進行額外處理。finalize本質上成為了快速回收的障礙者,可能導致你的物件經過多個垃圾收集週期才能被回收。
有人也許會問,我用System.runFinalization() 告訴JVM積極一點,是不是就可以了?也需有點用,但是問題在於,這還是不可預測、不能保證的,所以本質上還是不能指望。實踐中,因為finalize拖慢垃圾收集,導致大量物件堆積,也是一種典型的導致OOM的原因。
從另一個角度,我們要確保回收資源就是因為資源都是有限的,垃圾收集時間的不可預測,可能會極大加劇資源佔用。這意味著對於小號非常高頻的資源,千萬不要指望finalize去承擔資源釋放的主要職責,最多讓finalize微微最好的“守門員”,況且它已經暴露了如此多的問題。這也是為什麼我推薦,資源用完即顯示釋放,或者利用資源池來儘量重用。
fianlize還會掩蓋回收時的出錯資訊,我們看下面一段JDK的原始碼,擷取自java.lang.ref.Finalizer
private void runFinalizer(JavaLangAccess jla){
//...省略部分程式碼
try{
Object finalizee = this.get();
if(finalizee != null && !(finalizee instanceof java.lang.Enum)){
jla.invokeFinalize(finalizee);
finalizee = null;
}
}catch(Throwable x){}
super.clear();
}
是的,你沒有看錯,這裡的Throwable是被生吞了的!也就意味著一旦出現異常或者出錯,你得不到任何有效資訊。況且,java在finalize階段也沒有好的方式處理任何資訊,不然更加不可預測。
三、有什麼機制可以替換finalize嗎?
java平臺目前在逐步使用java.lang.ref.Cleaner來替換掉原來的finalize實現。Cleaner的實現利用了幻象引用(PhantomReference),這是一種常見的所謂post-mortem清理機制。利用幻象引用和引用佇列,我們可以保證物件被徹底銷燬前做一些類似資源回收的工作,比如關閉檔案描述符(作業系統有限的資源),它比finalize更加輕量、更加可靠。
吸取了finalize的教訓,每個Cleaner的操作都是獨立的,它有自己的執行執行緒,所以可以避免意外死鎖等問題。
實踐中,我們可以為自己的模組構建一個Cleaner,然後實現相應的清理邏輯。下面是JDK自身提供的樣例程式:
public class CleaningExample implements AutoCloseable{
private static final Cleaner cleaner = <cleaner>;
static class State implements Runnable{
State(...){
//initialize State needed for cleaning action
}
public void run(){
//cleanup action accessing State, executed at most once
}
}
private final State;
private final Clean.Cleanable cleanable;
public CleaningExample(){
this.state = new State(...);
this.cleanable = cleaner.register(this, state);
}
public void close(){
cleanable.clean();
}
}
注意,從可預測性的角度來判斷,Cleaner或者幻象引用改善的程式仍然是有限的,比如由於種種原因導致幻象引用堆積,同樣會出現問題。所以,Cleaner適合作為一種最後的保證手段,而不是完全依賴Cleaner進行資源回收,不然我們就要再做一遍finalize的噩夢了。
我也注意到,很多第三方庫自己直接利用幻象引用定製資源回收,比如廣泛使用的MySQL JDBC driver之一的mysql-connector-j,就利用了幻象引用機制。幻象引用也可以進行類似鏈條式依賴關係的動作,比如,進行總量控制的場景,保證只有連線被關閉,相應資源被回收,連線池才能建立新的連線。
另外,這種資源如果稍有不慎添加了對資源的強引用關係,就會導致迴圈引用關係,前面提到的MySQL JDBC就在特定模式下有這種問題,導致記憶體洩漏。上面的示例程式碼中,將State定義為static,就是為了避免普通的內部類隱含著對外部物件的強引用,因為那樣會使外部物件無法進入幻象可達的狀態。