文章目錄
圖一:Java運作時資料區域示意圖

Java 記憶體運作時區域中的程式計數器、虛拟機棧、本地方法棧随線程而生滅;是以這幾個區域的記憶體配置設定和回收都具備确定性,不需要過多考慮回收的問題,因為方法結束或者線程結束時,記憶體自然就跟随着回收了。
而 Java 堆不一樣,一個接口中的多個實作類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,隻有在程式處于運作期間時才能知道會建立哪些對象,這部分記憶體的配置設定和回收都是動态的,Java的垃圾回收機制所關注的是這部分記憶體。
Java垃圾回收機制具有如下特征 :
- 垃圾回收機制隻負責回收堆記憶體中的對象,不會回收任何實體資源(例如資料庫連接配接、網絡IO等資源) 。
- 程式無法精确控制垃圾回收的運作,垃圾回收會在合适的時候進行。當對象永久性地失去引用後,系統就會在合适的時候回收它所占的記憶體 。
- 在垃圾回收機制回收任何對象之前,總會先調用它的 finalize()方法,該方法可能使該對象重新複活(讓一個引用變量重新引用該對象) ,進而導緻垃圾回收機制取消回收。
當 一個對象在堆記憶體中運作時,根據它被引用變量所引用的狀态,可以把它所處的狀态分成如下三種:
- 可達狀态 : 當 一個對象被建立後,若有一個以上的引用變量引用它,則這個對象在程式中處于可達狀态,程式可通過引用變量來調用該對象的執行個體變量和方法。
- 可恢複狀态:如果程式中某個對象不再有任何引用變量引用它 ,它就進入了可恢複狀态。在這種狀态下,系統的垃圾回收機制準備回收該對象所占用的記憶體,在回收該對象之前,系統會調用所有可恢複狀态對象的 finalize()方法進行資源清理 。 如果系統在調用 finalize()方法時重新讓一個引用變量引用該對象,則這個對象會再次變為可達狀态;否則該對象将進入不可達狀态。
- 不可達狀态:當對象與所有引用變量的關聯都被切斷,且系統已經調用所有對象的 finalize()方法後依然沒有使該對象變成可達狀态,那麼這個對象将永久性地失去引用,最後變成不可達狀态。隻有當一個對象處于不可達狀态時,系統才會真正回收該對象所占有的資源 。
圖二顯示了對象的三種狀态的轉換示意圖 。
圖二:對象狀态轉換示意圖
例如,下面程式簡單地建立了兩個字元串對象,并建立了 一個引用變量依次指向兩個對象 。
StatusTranfer.java
public class StatusTranfer {
public static void test () {
String a = new String("馬作的盧飛快"); //①
a = new String("弓如霹靂弦驚") ; //②
}
public static void main(String []args) {
test(); //③
}
}
- 當程式執行 test 方法的①代碼時,代碼定義了 一個 a 變量,并讓該變量指向"馬作的盧飛快"字元串,該代碼執行結束後,"馬作的盧飛快"字元串對象處于可達狀态 。
- 當程式執行了 test 方法的②代碼後,代碼再次建立了"弓如霹靂弦驚"字元串對象,并讓 a 變量指向該對象 。 此時,"馬作的盧飛快"字元串對象處于可恢複狀态,而"弓如霹靂弦驚"字元串對象處于可達狀态。
一個對象可以被一個方法的局部變量引用, 也可以被其他類的類變量引用 ,或被其他對象的執行個體變量引用 。
- 當某個對象被其他類的類變量引用時, 隻有該類被銷毀後,該對象才會進入可恢複狀态;
- 當某個對象被其他對象的執行個體變量引用時,隻有當該對象被銷毀後 ,該對象才會進入可恢複狀态 。
當一個對象失去引用後,系統何時調用它的 finalize()方法對它進行資源清理,何時它會變成不可達狀态,系統何時回收它所占有的記憶體,對于程式完全透明。程式隻能控制一個對象何時不再被任何引用變量引用,但不能控制它何時被回收 。
雖然程式無法精确控制 Java 垃圾回收的時機,但可以強制系統進行垃圾回收一一這種強制隻是通知系統進行垃圾回收,但系統是否進行垃圾回收依然不确定 。 大部分時候,程式強制系統垃圾回收後是有效的。
強制系統垃圾回收有如下兩種方式 :
- 調用 System 類的 gc()靜态方法: System.gc()
- 調用 Runtime 對象的 gc()執行個體方法: Runtime.getRuntime().gc()
下面程式建立了 4 個匿名對象 , 每個對象建立之後立即進入可恢複狀态,等待系統回收 , 但直到程式退出,系統依然不會回收該資源 。
GcTest.java
public class GcTest {
public static void main(String[] args) {
for (int i = 0 ; i < 4 ; i++) {
new GcTest();
}
}
public void finalize () {
System.out.println (" 系統正在清理 GcTest 對象的資源 . . . ") ;
}
}
編譯、運作上面程式 , 看不到任何輸出,可見直到系統退出 , 系統都不曾調用 GcTest 對象的 finalize()方法 。 但如果将程式修改成如下形式 :
public class GcTest {
public static void main(String[] args) {
for (int i = 0 ; i < 4 ; i++) {
new GcTest();
// 下面兩行代碼的作用 完全相同,強制系統進行垃圾回收
//System.gc() ;
Runtime.getRuntime() . gc();
}
}
public void finalize () {
System.out.println (" 系統正在清理 GcTest 對象的資源 . . . ") ;
}
}
上面程式與前一個程式相比,增加了強制系統進行垃圾回收代碼 。 編譯上面程式,使用如下指令來運作此程式 :
java -verbose:gc GcTest
圖三:垃圾回收的運作提示資訊
運作 Java 指令時指定 -verbose :gc 選項,可以看到每次垃坡回收後的提示資訊,如圖三所示 。
從圖三 中可以看出,每次調用了 Runtime.getRuntime(). gc()代碼後 , 系統垃圾回收機制 還是"有所動作"的,可以看出垃圾回收之前、回收之後的記憶體占用對 比 。
雖然圖三顯示了程式強制垃圾回收的效果,但仍然要認識到這種強制隻是建議系統立即進行垃坡回收 , 系統完全有可能并不立即進行垃圾回收,垃圾回收機制也不會對程式的建議完全置之不理 : 垃圾回收機制會在收到通知後,盡快進行垃圾回收 。
在垃圾回收機制回收某個對象所占用的記憶體之前,通常要求程式調用适當的方法來清理資源 , 在沒有明确指定清理資源的情況下, Java 提供了預設機制來清理該對象的資源,這個機制就是 finalize()方法 。
該方法是定義在 Object 類裡的執行個體方法,方法原型為 :
protected void finalize() throws Throwable
當 finalize()方法傳回後,對象消失,垃圾回收機制開始執行 。方法原型中的 throws Throwable 表示它可以抛出任何類型的異常 。
任何 Java 類都可以重寫 Object 類的 finalizeO方法,在該方法中清理該對象占用的資源 。 如果程式終止之前始終沒有進行垃圾回收,則不會調用失去引用對象的 finalize()方法來清理資源 。
垃圾回收機制何時調用對象的 finalizeO方法是完全透明的,隻有當程式認為需要更多的額外記憶體時,垃圾回收機制才會進行垃坡回收 。 是以,完全有可能出現這樣一種情形:某個失去引用的對象隻占用了少量記憶體,而且系統沒有産生嚴重的記憶體需求,是以垃圾回收機制并沒有試圖回收該對象所占用的資源,是以該對象的自finalize()方法也不會得到調用。
finalize()方法具有如下 4 個特點 :
- 永遠不要主動調用某個對象的 finalize()方法,該方法應交給垃圾回收機制調用 。
- finalize()方法何時被調用,是否被調用具有不确定性 ,不要把自finalize()方法當成一定會被執行的方法 。
- 當 JVM 執行可恢複對象的 finalize()方法時,可能使該對象或系統中其他對象重新變成可達狀态。
- 當執行 finalize()方法時出現異常時,垃圾回收機制不會報告異常,程式繼續執行 。
下面程式示範了如何在 finalize()方法裡複活自身,并可通過該程式看出垃圾回收的不确定性。
FinalizeTest.java
public class FinalizeTest {
private static FinalizeTest ft = null;
public void info() {
System.out.println( "測試資源清理的 finalize 方法 " );
}
public static void main(String[] args) throws Exception{
// 建立 FinalizeTest 對象立即進入可恢複狀态
new FinalizeTest() ;
// 通知系統進行資源回收
System.gc(); //①
// 強制垃圾回收機制調用可恢複對象的 finalize ()方法
// Runtime.getRuntime() . runFinalization() ; //②
System.runFinalization(); //③
ft.info();
}
public void finalize() {
// 讓 ft 引用到試圖回收的可恢複對象,即可恢複對象重新變成可達
ft = this;
}
}
上面程式中定義了 一個 FinalizeTest 類,重寫了該類的 finalizeO方法,在該方法中把需要清理的可恢複對象重新賦給 a 引用變量,進而讓該可恢複對象重新變成可達狀态。
上面程式中的 main()方法建立了 一個 FinalizeTest 類的匿名對象,因為建立後沒有把這個對象賦給任何引用變量,是以該對象立即進入可恢複狀态 。 進入可恢複狀态後:
- 系統調用①号字代碼通知系統進行垃圾回收
- ②号代碼強制系統立即調用可恢複對象的 finalize()方法,再次調用位對象的 info()方法 。編譯、運作上面程式 , 看到 info()方法被正常執行 。
-
如果删除①行代碼,取消強制垃圾回收 。 再次編譯、運作上面程式,将會看到如圖四所示的結果 。
圖四:調用info()方法時引發空指針異常
從圖四所示的運作結果可以看 出,如果取消①号代碼,程式并沒有通知系統開始執行垃圾回收(而且程式記憶體也沒有緊張 ) ,是以系統通常不會立即進行垃圾回收,也就不會調用 FinalizeTest對象的 fmalize()方法,這樣 FinalizeTest 的ft類變量将依然保持為 null,這樣就導緻了空指針異常 。
上面程式中②号代碼和③号代碼都用于強制垃圾回收機制調用可恢複對象的 finalize()方法,如果程式僅執行 System.gc(); 代碼,而不執行②号或③号代碼一一由于 JVM垃圾回收機制的不确定性,JVM往往并不立即調用可恢複對象的 finalize()方法,這樣 FinalizeTest 的ft類變量可能依然為 null ,可能依然會導緻空指針異常。
對大部分對象而言 ,程式裡會有一個引用變量引用該對象,這是最常見的引用方式 。 除此之外,還有軟引用、弱引用、虛引用。
強引用就是指在程式代碼之中普遍存在的,類似 "Object obj = new Object()” 這類的引用,隻要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。
軟引用是用來描述一些還有用但并非必需的對象。對于軟引用關聯着的對象,在系統将要發生記憶體溢出異常之前,将會把這些對象列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出異常。在JDK 1.2之後,提供了 SoftReference類來實作軟引用。
弱引用也是用來描述非必需對象的,但是它的強度比軟引用更弱一些,被弱引用 關聯的對象隻能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當 前記憶體是否足夠,都會回收掉隻被弱引用關聯的對象。在JDK 1.2之後,提供WeakReference類來實作弱引用。
虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛 引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實 例。為一個對象設定虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一 個系統通知。在JDK 1.2之後,提供了 PhantomRcfcrcncc類來實作虛引用。
上面三個引用類都包含了 一個 get()方法,用于擷取被它們所引用的對象 。
引用隊列由 java. lang.ref.ReferenceQueue 類表示,它用于儲存被回收後對象的引用 。 當聯合使用軟引用、弱引用和引用隊列時,系統在回收被引用的對象之後,将把被回收對象對應的引用添加到關聯的引用隊列中。與軟引用和弱引用不同的是,虛引用在對象被釋放之前,将把它對應的虛引用添加到它關聯的引用隊列中,這使得可以在對象被回收之前采取行動 。
軟引用和弱引用可以單獨使用,但虛引用不能單獨使用,單獨使用虛引用沒有太大的意義。虛引用的主要作用就是跟蹤對象被垃圾回收的狀态,程式可以通過檢查與虛引用關聯的引用隊列中是否已經包含了該虛引用,進而了解虛引用所引用的對象是否即将被回收 。
下面程式示範了弱引用所引用的對象被系統垃圾回收過程 :
ReferenceTest.java
public class ReferenceTest {
public static void main(String[] args) throws Exception {
//建立一個字元串對象
String str = new String( "瘋狂Java講義 " ) ;
//建立一個弱引用,讓此弱引用引用到 " 瘋狂 Java 講義 " 字元串
WeakReference wr = new WeakReference(str) ; //①
//切斷 str 引用和 "瘋狂 Java 講義 " 字元串之間的引用
str = null; // ②
//取出弱引用所引用的對象
System.out.println(wr.get()); //③
//強制垃圾回收
System.gc ();
System.runFinalization ();
//再次取出弱引用所引用的對象
System.out.println(wr.get()); //④
}
}
上面程式先建立了 一個"瘋狂 Java 講義"宇符串對象,并讓 5位引用變量引用它,執行①行粗體字代碼時,系統建立了 一個弱引用對象,并讓該對象和 str引用同一個對象 。 當程式執行到②行代碼時,程式切斷了str和 "瘋狂 Java 講義 "字元串對象之間 的引用關系 。 此時系統記憶體如圖 6.10 所示 。
僅被弱引用引用的字元串對象
當程式執行到③号代碼時 ,由于本程式不會導緻記憶體緊張 ,此時程式通常還不會回收弱引用wr 所引用的對象,是以在③号代碼處可以看到輸出 "瘋狂 Java 講義"字元串 。執行到③号代碼之後,程式調用了 System.gc(); 和 System.runFinalization(); 通知系統進行垃圾回收,如果系統立即進行垃圾回收,那麼就會将弱引用 WT 所引用的對象回收 。 接下來在④号字代碼處将看到輸出 nul l 。
下面程式與上面程式基本相似,隻是使用了虛引用來引用字元串對象,虛引用無法擷取它引用的對象。下面程式還将虛引用和引用隊列結合使用,可以看到被虛引用所引用的對象被垃圾回收後,虛引用将被添加到引用隊列中 。
PhantomReferenceTest.java
public class PhantomReferenceTest {
public static void main(String[] args) throws Exception{
//建立一個字元串對象
String str = new String("瘋狂 Java 講義 " ) ;
//建立一個引用隊列
ReferenceQueue rq = new ReferenceQueue();
// 建立一個虛引用,讓此虛引用引用到"瘋狂 Java 講義"字元串
PhantomReference pr = new PhantomReference (str , rq);
// 切斷 str 引用和 " 瘋狂 Java 講義 " 字元串之間的引用
str = null;
// 取出虛 引用所引用的對象,并不能通過虛引用擷取被引用的對象,是以此處輸出 null
System. out .println(pr . get()) ; // ①
// 強制垃圾回收
System . gc () ;
System . runFinalization();
//垃圾回收之後 ,虛引用将被放入引用隊列中
// 取出引用 隊列中最先進入隊列的引用與 pr 進行比較
System. out.println(rq.poll()==pr); // ②
}
}
因為系統無法通過虛引用來獲得被引用的對象,是以執行①處的輸出語句時,程式将輸出 null (即使此時并未強制進行垃圾回收)。當程式強制垃圾回收後,隻有虛引用引用的字元串對象将會被垃圾回收,當被引用的對象被回收後,對應的虛引用将被添加到關聯的 引用隊列中,因而将在②代碼處看到輸出 true 。
使用這些引用類可以避免在程式執行期間将對象留在記憶體中。如果以軟引用、弱引用或虛引用的方式引用對象,垃圾回收器就能夠随意地釋放對象 。如果希望盡可能減小程式在其生命周期中所占用的記憶體大小時,這些引用類就很有用處。
必須指出:要使用這些特殊的引用類,就不能保留對對象的強引用:如果保留了對對象的強引用,就會浪費這些引用類所提供的任何好處 。
由于垃圾回收的不确定性,當程式希望從軟、弱引用中取出被引用對象時,可能這個被引用對象己經被釋放了。如果程式需要使用那個被引用的對象,則必須重新建立該對象 。 這個過程可以采用兩種方式完成,下面代碼顯示了其中一種方式。
// 取出弱引用所引用的對象
obj = wr.get() ;
//如果取出的對象為 null
if (obj == null){
// 重新建立一個新的對象,再次讓弱引用去引用該對象
wr = new WeakReference(recreatelt()) ; // ①
//取出弱引用所引用的對象,将其賦給 obj 變量
bj = wr. get () ; // ②
}
// 操作 obj 對象
// 再次切斷 obj 和對象之間的關聯
obj =null ;
下面代碼顯示了另 一種取出被引用對象的方式 。
// 取出弱引用所引用的對象
obj = wr.get();
// 如果取出的對象為 null
if (obj == null){
//重新建立一個新的對象,并使用強引用來引用它
obj = recreatelt();
// 取出弱引用所引用的對象,将其賦給 obj 變量
wr = new WeakReference(obj);
}
//操作 obj 對象
//再次切斷。同和對象之間的關聯
obj = null;
上面兩段代碼采用的都是僞碼,其中 recreatelt()方法用于生成一個 obj 對象 。 這兩段代碼都是先判斷 obj 對象是否已經被回收 ,如果己經被回收,則重新建立該對象 。 如果弱引用引用的對象己經被垃圾回收釋放了,則重新建立該對象 。 但第一段代碼存在一定的問題:當 if 塊執行完成後, obj 還是有可為 null 。 因為垃圾回收的不确定性,假設系統在①和②行代碼之間進行垃坡回收,則系統會再次将 wr所引用的對象回收,進而導緻 obj 依然為 null。第二段代碼則不會存在這個問題,當 if 塊執行結束後,obj 一 定不為 null 。
參考:
【1】:《瘋狂Java講義》
【2】:《深入了解Java虛拟機:JVM進階特性與最佳實踐》
【3】:
https://www.cnblogs.com/czwbig/p/11127124.html【4】:
https://www.cnblogs.com/czwbig/p/11127159.html