垃圾回收機制具有如下特徵:
- 垃圾回收機制只負責回收對內存中的對象,不會回收任何物理資源,例如數據連接,網絡IO等
- 程序無法精確的控制垃圾回收的運行,當對象永久的失去引用,系統就會在合適的時候回收他所佔的內存
- 在垃圾回收機制回收任何對象之前,總會先調用他的finalize()方法,該方法可能使該對象重新復活,也就是重新獲得引用,從而導致垃圾回收機制取消回收
對象在內存中的狀態
當一個對象在堆內存中運行時,根據它被引用變量引用的狀態,可以把它分成三種情況:
- 可達狀態:一個對象被創建後,存在一個以上的引用變量引用他,這個對象在程序中處於可達狀態,可以通過引用變量來調用對象的實例變量和方法
- 可恢復狀態:一個對象不存在任何引用變量引用它,就進入了可恢復狀態,垃圾回收機制準備回收該對象所佔內存,在回收之前,系統會調用所有可恢復狀態對象的finalize()方法進行資源清理,如果在調用finalize()方法時重新讓一個引用變量引用該對象,則這個對象在此變爲可達狀態,否則進入不可達狀態
- 不可達狀態:一個對象的所有引用變量的關聯都被切斷,且系統已經調用所有對象的finalize()方法後依然沒有使該對象變成可達狀態,那麼這個對象永久地失去引用,最後變成不可達,只有對象處於不可達時,系統纔會真正的回收該對象所佔資源
public class StatusTranfer
{
public static void test()
{
var a = new String("輕量級Java EE企業應用實戰");
a = new String("瘋狂Java講義");
}
public static void main(String[] args)
{
test();
}
}
執行test()的時候,首先創建了一個引用變量a,a指向的堆內存的值是"輕量級Java EE企業應用實戰",這個時候a是可達狀態,之後又創建了一個對象,並且讓a指向了它,那麼在堆內存中"瘋狂Java講義"就成了可達狀態,而"輕量級Java EE企業應用實戰"變爲可恢復狀態
- 當某個對象被其他類的類變量引用時,只有該類被銷燬了,被應用對象才進入了可恢復狀態
- 當某個對象被其他對象的實例引用時,只有當該對象被銷燬後,被引用對象纔會進入可恢復狀態
強制垃圾回收的方法
程序可以控制對象何時不再被任何引用變量引用,但無法精確控制Java垃圾回收的時機,雖然可以強制系統進行垃圾回收,但這種強制回收也只是通知系統進行垃圾回收,系統何時去做依然不確定,強制回收兩種方式
- 調用System類的gc()靜態方法:System.gc()
- 調用Runtime對象的gc()實例方法:Runtime.getRuntime().gc()
public class GcTest
{
public static void main(String[] args)
{
for (var i = 0; i < 4; i++)
{
new GcTest();
}
}
public void finalize()
{
System.out.println("系統正在清理GcTest對象的資源...");
}
}
public class GcTest
{
public static void main(String[] args)
{
for (var i = 0; i < 4; i++)
{
new GcTest();
// 下面兩行代碼的作用完全相同,強制系統進行垃圾回收
// System.gc();
Runtime.getRuntime().gc();
}
}
public void finalize()
{
System.out.println("系統正在清理GcTest對象的資源...");
}
}
編譯後使用命令java -verbose:gc GcTest執行的結果如下:
D:\BaiduNetdiskDownload\CrazyJava\codes\06\6.10>java -verbose:gc GcTest
[0.012s][info][gc] Using G1
[0.136s][info][gc] GC(0) Pause Full (System.gc()) 1M->0M(8M) 6.900ms
[0.140s][info][gc] GC(1) Pause Full (System.gc()) 0M->0M(8M) 3.102ms
[0.144s][info][gc] GC(2) Pause Full (System.gc()) 0M->0M(8M) 2.971ms
[0.148s][info][gc] GC(3) Pause Full (System.gc()) 0M->0M(8M) 2.774ms
系統正在清理GcTest對象的資源...
D:\BaiduNetdiskDownload\CrazyJava\codes\06\6.10>
銷燬對象
在垃圾回收機制回收對象佔用的內存之前,程序會調用適當的方法來清理資源,如果沒有明確指定清理資源的話,Java提供了默認機制,這個機制就是finalize()方法,該方法是定義在Object類中protected void finalize() throws Throwable
當該方法返回後,對象便消失,垃圾回收機制開始執行,然而只有當程序認爲需要更多的額外內存時,垃圾回收機制纔會進行垃圾回收,如果程序不需要額外的內存,即便對象失去了引用,垃圾回收機制也不會試圖回收該對象所佔用的資源,同時該對象的finalize()方法也不會得到調用也不會被
- 不可主動調用某個對象的finalize()方法,由垃圾回收機制調用
- finalize()方法被調用具有不確定性,不要把它當成一定會被執行的方法
- JVM執行可恢復對象的finalize()方法時,可能使該對象或者系統中其他對象重新變成可達狀態
- JVM執行finalize()方法時出現異常,垃圾回收機制不會報異常,程序繼續執行
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類,重寫該類的finalize()方法,在該方法中把需要清理的可恢復對象重寫賦給了ft引用變量,從而讓該可恢復對象重新變成了可達狀態
如果去掉 System.gc();,程序不通知系統開始執行垃圾回收,並且程序內存也不緊張,因此係統就不會立即執行垃圾回收,也就不會調用finalize()方法,這樣ft就還是null,就導致了空指針異常
如果程序執行了 System.gc();,但是沒有執行Runtime.getRuntime().runFinalization();或者System.runFinalization(); 由於JVM垃圾回收機制的不確定性,JVM可能不會立即調用finalize()方法,ft還是可能爲null,也還是會遇到空指針異常
對象的強、軟、弱和虛引用
通常情況,程序中的對象會有一個引用變量引用它,實際上Java的java.lang.ref包下還提供了3個類:SoftReference/PhantomReference和WeakReference,分別代表了系統對對象的3鍾引用方式:軟引用、虛引用和弱引用,算上最常見的一種,一共四種方式。
強引用
Java中最常見的一種方式,程序創建一個對象,並把這個對象賦給一個引用變量,程序通過該引用變量操作實際對象,當一個對象被一個或一個以上的引用變量所引用時,它就處於可達狀態,不可能被系統垃圾回收機制回收
軟引用
軟引用需要通過SoftReference類來實現,當一個對象只有軟引用時,它有可能被垃圾回收機制回收。對於只有軟引用的對象來說,當系統內存空間足夠的時候,不會被系統回收,程序也可以使用該對象,但是當系統內存空間不足時,系統可能會回收它。
軟引用通常用於對內存敏感程序中
弱引用
弱引用需要通過WeakReference類來實現,弱引用和軟引用很類似,但弱引用的引用級別更低,對於只有弱引用的對象來說,當系統垃圾回收機制運行時,不管系統內存是否足夠,總會回收該對象所佔用的內存
虛引用
虛引用需要通過PhantomReference類來實現,虛引用類似於沒有引用,對象甚至不知道虛引用的存在。虛引用主要用於跟蹤對象被垃圾回收的狀態,它不能單獨使用必須和引用隊列(ReferenceQuene)聯合使用,程序可以通過檢查與虛引用關聯的引用隊列中是否已經包含了該虛引用,從而瞭解虛引用所引用的對象是否即將被回收。
引用隊列(ReferenceQuene)
引用隊列由java.lang.ref.ReferenceQuene類表示,它用於保存被回收對象的引用;當聯合使用軟引用、弱引用和引用隊列時,系統在回收被引能的對象後,將把被回收對象對應的引用添加到關聯的引用隊列中
與軟引用和弱引用不同,虛引用在對象被釋放之前,將把它對應的虛引用添加到它關聯的引用隊列中,這樣可以在對象被回收之前採取一些措施
import java.lang.ref.*;
public class ReferenceTest
{
public static void main(String[] args)
throws Exception
{
// 創建一個字符串對象
var str = new String("我醉欲眠卿且去 明朝有意抱琴來");
// 創建一個弱引用,讓此弱引用引用到"我醉欲眠卿且去 明朝有意抱琴來"字符串
var wr = new WeakReference(str);
// 切斷str引用和"我醉欲眠卿且去 明朝有意抱琴來"字符串之間的引用
str = null;
// 此程序通常不會回收弱引用wr所應用的對象,因此還可以取出弱引用所引用的對象
System.out.println(wr.get());
// 強制垃圾回收
System.gc();
System.runFinalization();
// 再次取出弱引用所引用的對象,輸出null
System.out.println(wr.get());
}
}
var str = new String("我醉欲眠卿且去 明朝有意抱琴來");
此行代碼不能用String str = "我醉欲眠卿且去 明朝有意抱琴來";
來代替,採用String str...
的形式系統會用常量池來管理這個字符串直接量,並且是強引用,系統不會回收這個字符串直接量
import java.lang.ref.*;
public class PhantomReferenceTest
{
public static void main(String[] args)
throws Exception
{
// 創建一個字符串對象
var str = new String("我醉欲眠卿且去 去 去你媽地");
// 創建一個引用隊列
var rq = new ReferenceQueue();
// 創建一個虛引用,讓此虛引用引用到"我醉欲眠卿且去 去 去你媽地"字符串
var pr = new PhantomReference(str, rq);
// 切斷str引用和"瘋狂Java講義"字符串之間的引用
str = null;
// 取出虛引用所引用的對象,並不能通過虛引用獲取被引用的對象,所以此處輸出null
System.out.println(pr.get());
// 強制垃圾回收
System.gc();
System.runFinalization();
// 垃圾回收之後,虛引用將被放入引用隊列中
// 取出引用隊列中最先進入隊列中的引用與pr進行比較,此處輸出true
System.out.println(rq.poll() == pr);
}
}
系統無法通過虛引用獲得被引用的對象,即便虛引用所引用的對象未被強制垃圾回收,當程序強制垃圾回收後,只是虛引用所引用的對象被回收,對應的虛引用將被添加到關聯的引用隊列中
如果要使用這些特殊的引用類,就不能保留對對象的強引用,否則這些引用將失去效果
// 僞代碼
obj = wr.get();
if (obj == null)
{
wr = new WeakReference(recreateIt());
obj = wr.get();
}
// 僞代碼
obj = wr.get();
if (obj == null)
{
obj = recreateIt();
wr = new WeakReference(obj);
}
由於垃圾回收的不確定性,當程序系統從軟引用、弱引用中取出被引用對象時,可能這個對象已經被釋放了,因此在取對象的時候就需要判斷
第一段代碼存在風險,如果垃圾回收在代碼obj = wr.get();和wr = new WeakReference(recreateIt());之間進行,那麼系統會再次將wr所引用的對象回收,obj取出來還是null
第二段代碼不存在這個風險