Java 物件與垃圾回收

NO IMAGE

當程式建立物件、陣列等引用型別實體時,系統都會在堆記憶體中為之分配一塊記憶體區,物件就儲存在這塊記憶體區中,當這塊記憶體不再被任何引用變數引用時,這塊記憶體就變成垃圾,等待垃圾回收機制進行回收。垃圾回收機制具有如下特徵。

垃圾回收機制只負責回收記憶體中的物件,不會回收任何物理資源(例如資料庫連線、網路IO等資源)

程式無法精確控制垃圾回收的執行,垃圾回收會在合適的時候進行。

在垃圾回收機制回收任何物件之前,總會先呼叫它的finalize()方法,該方法可能使該物件重新復活(讓一個引用變數重新引用該物件),從而導致垃圾回收機制取消回收。

物件在記憶體中的狀態

當一個物件在堆記憶體中執行時,根據它被引用變數所引用的狀態,可以把它所處的狀態分成如下三種:

可達狀態:當一個物件被建立後,若有一個以上的引用變數引用它,則找個物件在程式中處於可達狀態,程式可通過引用變數來呼叫該物件的例項變數和方法。

可恢復狀態:如果程式中某個物件不再有任何引用變數引用它,它就進入了可恢復狀態。在這種狀態下,系統的垃圾回收機制準備回收該物件所佔用的記憶體,在回收該物件之前,系統會呼叫所有可恢復狀態物件的finalize()方法進行資源清理。如果系統在呼叫finalize()方法時重新讓一個引用變數引用該物件,則這個物件會再次變成可達狀態;否則該物件將進入不可達狀態。

不可達狀態:當物件與所有引用變數的關聯都被切斷,且系統已經呼叫所有物件的finalize()方法後依然沒有使該物件變成可達狀態,那麼這個物件將永久性地失去引用,最後變成不可達狀態。只有當一個物件處於不可達狀態時,系統才會真正回收該物件所佔有的資源。

clipboard.png

public class StatusTranfer 
{
public static void test() 
{
String a = new String("知乎、掘金、SegmentFault");
a = new String("Java");
}
public static void main(String[] args) 
{
test();
}
}

當程式執行test方法的第一行程式碼時,程式碼定義了一個a變數,並讓該變數指向”知乎、掘金、SegmentFault”字串,該程式碼執行結束後”知乎、掘金、SegmentFault”字串物件處於可達狀態。

當程式執行test方法的第二行程式碼時,程式碼再次建立了”Java”字串物件,並讓a變數指向該物件。此時”知乎、掘金、SegmentFault”字串物件處於可恢復狀態,而”Java”字串處於可達狀態。

一個物件可以被一個方法的區域性變數引用,也可以被其他類的類變數引用,或被其他物件的例項變數引用。當某個物件被其他類的類變數引用時,只有該類被銷燬後,該物件才會進入可恢復狀態;當某個物件被其他物件的例項變數引用時,只有當該物件被銷燬後,該物件才會進入可恢復狀態。

強制垃圾回收

當一個物件失去引用後,系統何時呼叫它的finalize()方法對它進行資源清理,何時它會變成不可達狀態,系統何時回收它所佔有的記憶體,對於程式完全透明。程式只能控制一個物件何時不再被任何引用變數引用,絕不能控制它何時被回收。

程式強制系統垃圾回收與如下兩種方式

呼叫System類的gc()靜態方法:System.gc()

呼叫Runtime物件的gc()例項方法:Runtime.getRuntime().gc()

finalize方法

在垃圾回收機制回收某個物件所佔用的記憶體之前,通常要求程式呼叫適當的方法來清理資源,在設有明確清理資源的情況下,Java提供了預設機制來清理該物件的資源,這個機制就是finalize()方法。該方法是定義在Object類裡的例項方法

protected void finalize() throws Throwable

finalize()方法具有如下4個特點:

永遠不要主動呼叫某個物件的finalize()方法,該方法應交給垃圾回收機制呼叫。

finalize()方法何時被呼叫,是否被呼叫具有不確定性,不要把finalize()方法當成一定會被執行的方法。

當JVM執行可恢復物件的fianlize()方法時,可能使該物件或系統中其他物件重新程式設計可達狀態。

當JVM執行finalize()方法時出現異常時,垃圾回收機制不會報告異常,程式繼續執行。

public class FinalizeTest 
{
private static FinalizeTest ft = null;
public void info()
{
System.out.println("測試資源清理的finalize方法");
}
public static void main(String[] args) 
{
//建立FinalizeTest物件立即進入可恢復狀態
new FinalizeTest();
//通知系統進行資源回收
System.gc();
//強制垃圾回收機制呼叫可恢復物件的finalize()方法
Runtime.getRuntime().runFinalization();
System.runFinalization();
ft.info();
}
public void finalize() 
{
//讓tf引用到試圖回收的可恢復物件,即可恢復物件重新變成可達
ft = this;
}
}

物件的軟、弱和虛引用

強引用(StrongReference)

Java程式中最常見的引用方式。程式建立一個物件,並把這個物件賦給一個引用變數,程式通過該引用變數來操作實際的物件。當一個物件被一個或一個以上的引用變數所引用時,它處於可達狀態,不可能被系統垃圾回收機制回收。

軟引用(SoftReference)

通過SoftReference類來實現,當一個物件只有軟引用時,它有可能被垃圾回收機制回收。當系統記憶體空間足夠時,它不會被系統回收,程式也可使用該物件;當系統記憶體空間不足時,系統可能會回收它。軟引用通常用於對記憶體敏感的程式中。

弱引用(WeakReference)

通過WeakReference類實現,弱引用和軟引用很像,但弱引用的引用級別更低。對於只有弱引用的物件而已,當系統垃圾回收機制執行時,不管系統記憶體是否足夠,總會回收該物件所佔用的記憶體。當然,並不是說當一個物件只有弱引用時,它就會立即被回收——正如那些失去引用的物件一樣,必須等到系統垃圾回收機制執行時才會被回收。

虛引用(PhantomReference)

通過PhantomReference類實現,虛引用完全類似於沒有引用。虛引用對物件本身沒有太大影響,物件甚至感覺不到虛引用的存在。如果一個物件只有一個虛引用時,那麼它和沒有引用的效果大致相同。虛引用主要用於跟蹤物件被垃圾回收的狀態,虛引用不能單獨使用,虛引用必須和引用佇列(ReferenceQueue)聯合使用。程式可以通過檢查與虛引用關聯的引用佇列中是否已經包含了該虛引用,從而瞭解虛引用所引用的物件被系統垃圾回收過程。

上面三個引用類都包含了一個get()方法,用於獲取被它們所引用的物件。

引用佇列由java.lang.ref.ReferenceQueue類表示,它用於儲存被回收後物件的引用。當聯合使用軟引用、弱引用和引用佇列時,系統在回收被引用的物件之後,將把被回收物件的引用新增到關聯的引用佇列中。與軟引用和弱引用不同的是,虛引用在物件被釋放之前,將把它對應的虛引用新增到它關聯的引用佇列中,這使得可以在物件被回收之前採取行動。

public class ReferenceTest 
{
public static void main(String[] args) throws Exception
{
//建立一個字串物件
String str = new String("克利夫蘭騎士");
//建立一個弱引用,讓此弱引用引用到到"克利夫蘭騎士"字串
WeakReference wr = new WeakReference(str);     //①
//切斷str引用和"克利夫蘭騎士"字串之間的引用
str = null;                                    //②
//取出弱引用所引用的物件
System.out.println(wr.get());                  //③
//強制垃圾回收
System.gc();
System.runFinalization();
//再次取出弱引用所引用的物件
System.out.println(wr.get());                  //④
}
}

clipboard.png

當程式執行①行程式碼時,系統建立了一個弱引用物件,並讓該物件和str引用同一個物件。
當程式執行②行程式碼時,程式切斷了str和”克利夫蘭騎士”字串物件之間的引用關係。
當程式執行③行程式碼時,由於本程式不會導致記憶體緊張,此時程式通常還不會回收弱引用wr所引用的物件,因此在③號程式碼處可以看到輸出字串。
之後,呼叫System.gc();和System.runFinalization();通知系統進行垃圾回收,如果系統立即進行垃圾回收,那麼就會將弱引用wr所引用的物件回收。在④號程式碼處將看到輸出null。

採用String str = “克利夫蘭騎士”;程式碼定義字串時,系統會使用常量池來管理這個字串直接量(會使用強引用來引用它),系統不會回收這個字串直接量。

import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
public class PhantomReferenceTest 
{
public static void main(String[] args) throws Exception
{
//建立一個字串物件
String str = new String("邁阿密熱火");
//建立一個引用佇列
ReferenceQueue rq = new ReferenceQueue<>();
//建立一個虛引用,讓此虛引用引用到"邁阿密熱火"字串
PhantomReference pr = new PhantomReference(str, rq);
//切斷str引用和"邁阿密熱火"字串之間的引用
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(recreateIt());        //①
//取出弱引用所引用的物件,將其賦給obj變數
obj = wr.get();                                //②
}
...//操作obj物件
//再次切斷obj和物件之間的關聯
obj = null;

//取出弱引用所引用的物件
obj = wr.get();
//如果取出的物件為null
if (obj == null) 
{
//重新建立一個新的物件,再次強引用去引用該物件
obj = recreateIt();
//取出弱引用所引用的物件,將其賦給obj變數
wr = new WeakReference(obj);
}
...//操作obj物件
//再次切斷obj和物件之間的關聯
obj = null;

上面兩段虛擬碼,其中recreateIt()方法用於生成一個obj物件。
第一段程式碼存在一定問題:當if塊執行完成後,obj還是有可能為null。因為垃圾回收的不確定性,假設系統在①和②行程式碼之間進行垃圾回收,則系統會再次將wr所引用的物件回收,從而導致obj依然為null。
第二段程式碼則不會出現這個問題,當if塊執行結束後,obj一定不為null。