小菜最近在讀《Java與模式》一書時,發現關於單例模式的章節中有這樣一段話:
作者想表達的大意為:為了實現某個對象能夠持久在內存中,以供程序在整個運行周期都可以訪問,可以讓對象的某個成員變量持有一個指向自身的引用,來避免被回收。
成員變量想要被清空,需要等待對象被釋放,而對象被釋放需要沒有引用指向它,此時成員變量恰恰指向了對象本身,這看起來很不錯,形成了一個循環。
但實際上,這種說法是不准確的,容易讓讀者產生誤解。
請看下邊這段代碼:
1 package com.cnblogs.test; 2 3 public class SingletonTest { 4 5 public static void main(String[] args) { 6 7 //調用測試方法 8 test(); 9 //通知jvm回收無用資源 10 System.gc(); 11 12 System.out.println("main finalize ..."); 13 } 14 15 //測試方法 16 public static void test(){ 17 //創建A類的對象 18 A a = new A(); 19 //讓對象的成員變量指向其自身 20 a._a=a; 21 } 22 23 } 24 25 class A{ 26 27 //定義一個成員變量,用來保存對象本身 28 public A _a = null; 29 30 //對象被銷毀時執行的方法 31 protected void finalize() throws Throwable { 32 System.out.println("A finalize ..."); 33 } 34 }
簡單說明一下:
當在main方法中調用test方法時,test方法會創建一個A類的實例a,同時把實例a的堆區地址放在實例a的成員變量_a中,也就是在模擬成員變量持有指向自身對象的引用。
當以上步驟執行完成后,test方法結束, 由於a是局部變量,保存在方法棧中,會被立即釋放,不再指向A類的實例,但是我們剛剛完成了“自引用”,根據上邊的理論,有引用指向A類的實例,實例便不會被釋放,因此上邊程序的輸出結果是“main 方法執行完畢...”。
遺憾的是,結果並不是這樣,真實的輸出結果為:“main 方法執行完畢...A 被回收...”。
為什么會這樣?聽聽小菜的解釋。
單例模式創建的對象能夠一直存在於內存中不被釋放,並不只是由於持有一個自身的引用,本質是因為這個引用是靜態的!也就是說,如果成員變量是非靜態的,它持有一個自身的引用,那么這個對象還是會被回收。
“系統內至少保持一個對對象的引用”,這個引用指的是從棧區或方法區中發出的引用,也就是安全的引用。
類被實例化之后,是放在堆區中的,而我們是無法直接操作堆內存的!因此需要一個引用,指向堆區的某個區域,而這個引用,必須是從棧中(或方法區中)發出的,因為我們可以直接訪問棧內存,如果是從堆中發出的引用,是無意義的引用,我們根本訪問不到,因此會被回收。
類的成員變量恰恰是放在堆內存中,因此由類的成員變量持有一個對象的引用,這個引用是不安全的(不安全≠無效)!!
再舉個例子,如果棧區的a局部變量指向堆中的b對象,b對象的某個成員變量指向堆中的c對象。我們可以通過a訪問b,再由b訪問c。假如一旦a不再指向b,那么再也訪問不到b,c也不可能被訪問到,即便b此時仍然指向c,但都成了無法訪問的對象,b、c均會被jvm自動回收。
單例模式中成員變量是靜態的,它並不保存在堆內存中,而是在方法區中,是一塊持久的內存空間,不會被自動回收,因此指向自身的引用是安全的,自身不會被回收。
到此,如果我們把第一個例子中的成員變量改成靜態的,那么test方法執行結束后,A類的對象不會被回收,程序輸出結果為:“main 方法執行完畢...”。
最后提一句,堆區是整個程序共享的,因此可能會出現線程安全問題。