Java的內存回收——Java引用的種類

1、Java引用的種類

       Java是面向對象的編程語言,一個Java程序往往需要創建大量的Java類,然後對各Java類創建大量的Java對象,再調用這些Java對象的屬性和方法來操作它們。
       程序員需要通過關鍵字new創建Java對象,既可以視作爲Java對象申請內存空間,JVM會在堆內存中爲每個對象分配空間;當一個Java對象失去引用時,JVM的垃圾回收機制會自動清除它們,並回收它們所佔用的空間。
       Java內存管理包括內存分配(創建Java對象時)和內存回收兩個方面(回收Java對象時)。這兩方面的工作都是由JVM自動完成的,因此降低了Java程序員的學習難度,以至於使容易忽視內存分配的問題。但這兩方面的工作也加重了JVM的工作,從而使Java程序運行較慢。

1、1 對象在內存中的狀態

       對於JVM的垃圾回收機制來說,是否回收一個對象的標準在於:是否還有引用變量引用該對象?只要有引用變量引用該變量,垃圾回收機制就不會回收它。也就是說,當Java對象被創建出來之後,垃圾回收機制會實時地監控每個對象的運行狀態,包括對象的申請、引用、被引用、賦值等。當垃圾回收機制實時地監控到某個對象不再被引用變量所引用時,垃圾回收機制就會回收它所佔用的空間。
       基本上,可以把JVM內存中的對象引用理解成一種有向圖,把引用變量、對象都當成有向圖的頂點,將引用關係當成圖的有向邊,有向邊總是從引用端指向被引用的Java對象。因爲Java的所有對象都是由一條條線程創建出來的,因此可以把線程對象當成有向圖的起始頂點。
       對於單線程程序而言,整個程序只有一條main線程,那麼該圖就是以main進程爲頂點的有向圖。在這個有向圖中,main頂點可達的對象都處於可達狀態,垃圾回收機制不會回收它們;如果某個對象在這個有向圖中處於不可達狀態,那麼就認爲這個對象不再被引用,接下來垃圾回收機制就會主動回收它了。
class Node {
	Node next;
	String name;
	
	public Node(String name) {
		this.name = name;
	}
}

public class NodeTest {
	public static void main(String[] args) {
		Node n1 = new Node("一號節點");
		Node n2 = new Node("二號節點");
		Node n3 = new Node("三號節點");
		n1.next = n2;
		n3 = n2;
		n2 = null;
	}
}
       上面程序中定義了三個Node對象,並通過合適的引用關係把這三個Node對象組織在一起,下圖爲JVM中對應的有向圖。

       從main頂點開始,有一條路徑到達“第一個Node對象”,因此該對象處於可達狀態,垃圾回收機制不會回收它;從main頂點開始,有兩條路徑到達“第二個Node對象”,因此該對象也處於可達狀態,垃圾回收機制不會回收它;從main頂點開始,沒有路徑可以到達“第三個Node對象”,因此這個Java對象就變成了垃圾,接下來垃圾回收機制就會回收它。

       當一個對象在堆內存中運行時,根據它在對應有向圖中的狀態,可以把它所處的狀態分成如下三種。
  • 可達狀態:當一個對象被創建後,有一個以上的引用變量引用它。在有向圖可以從起始頂點導航到該對象,那麼它就處於可達狀態,程序可以通過引用變量來調用該對象的屬性和方法。
  • 可恢復狀態:如果程序中某個對象不再有任何引用變量引用它,它將先進入可恢復狀態,此時從有向圖的起始頂點不能導航到該對象。在這種狀態下,系統的垃圾回收機制準備回收該對象所佔用的內存。如果系統調用finalize方法重新讓一個以上的引用變量引用該對象,則這個對象會再次變爲可達狀態;否則,該對象將進入不可達狀態。
  • 不可達狀態:當對象的所有關聯都被切斷,且系統調用所有對象的finalize方法依然沒有使該對象變成可達狀態後,這個對象將永久性地失去引用,最後變成不可達狀態。只有當一個對象處於不可達狀態時,系統纔會真正回收該對象所佔有的資源。
public class StatusTranfer {
	public static void test() {
		String a = new String("Java對象1");  //①
		a = new String("Java對象2");  //②
	}
	public static void main(String[] args) {
		test();
	}
}
       當程序執行test方法的①行代碼時,代碼定義了a變量,並讓該變量指向“Java對象1”字符串。該代碼執行結束後,“Java對象1”字符串對象處於可達狀態。
       當程序執行test方法的②行代碼後,代碼再次定義了“Java對象2”字符串對象,並讓a變量指向該對象。此時,“Java對象1”字符串對象處於可恢復狀態,而“Java對象2”字符串對象處於可達狀態。
       一個對象可以被一個方法的局部變量引用,也可以被其他類的類變量引用,或者被其他對象的實例變量引用。當某個對象被其他類的類變量引用時,只有該類被銷燬後,該對象纔會進入可恢復狀態;當某個對象被其他對象的實例變量引用時,只有當引用該對象的對象被銷燬或變成不可達狀態後,該對象纔會進入不可達狀態。
       對垃圾回收機制來說,判斷一個對象是否可回收的標準在於該對象是否被引用。因此引用也是JVM進行內存管理的一個重要概念。爲了更好地管理對象的引用,從JDK1.2開始,Java在java.lang.ref包下提供了三個類:SoftReference、PhantomReference和WeakReference,分別代表了對對象的三種引用方法:軟引用、虛引用和弱引用。其實,在Java中還有強引用,總共四種對對象的引用。

1、2 強引用

       這是Java程序中最常見的引用方式,程序創建一個對象,並把這個對象賦給一個引用變量,這個引用變量就是強變量。當一個對象被一個或一個以上的強引用變量所引用時,它處於可達狀態,它不可能被系統垃圾回收機制回收。強引用時Java編程中廣泛使用的引用類型,被強引用所引用的Java對象絕不會被垃圾回收機制回收,即使內存非常緊張;即使有些Java對象以後永遠也不會被用到,JVM也不會回收強引用所引用的Java對象。

1、3 軟引用

        軟引用需要通過SoftReference來實現,當一個對象只具有軟引用時,它可能被垃圾回收機制回收。對於只有軟引用的對象而言,當系統內存空間足夠時,它不會被系統回收,程序也可使用該對象;當系統內存空間不足時,系統將會回收它。
       軟引用通常用於內存敏感的程序中,軟引用是強引用很好的替代。當系統內存空間充足時,軟引用和強引用沒有太大的區別;當系統內存空間不足時,被軟引用所引用的Java對象可以被垃圾回收機制回收,從而避免系統內存不足的異常。

1、4 弱引用

       弱引用和軟引用有點類似,區別在於弱引用所引用對象的生存期更短。對於只有弱引用的對象而言,當系統垃圾回收機制運行時,不管系統內存是否足夠,總會回收該對象所佔用的內存。但是並不是當一個對象只有弱引用時,它就會立即被回收,而是等到系統垃圾回收機制運行時纔會被回收。
import java.lang.ref.WeakReference;

public class WeakReferenceTest {
	public static void main(String[] args) {
		String str = new String("Java對象");
		WeakReference<String> wr = new WeakReference<String>(str);
		str = null;  //切斷str引用和"Java對象"字符之間的引用
		System.out.println(wr.get());   //①
		System.gc();  //強制垃圾回收
		System.runFinalization();
		System.out.println(wr.get());   //②
	}
}
輸出結果爲:
Java對象
null
       當執行str = null;之後,切斷了str和“Java對象”字符串對象之間的引用關係,此時只有一個弱引用對象引用字符串對象,程序中①行代碼依然可以輸出“Java對象”。如果系統垃圾回收機制啓動,只有弱引用的對象就會被清理掉。程序中②行代碼輸出null,這就表明對象已經被清理了。
注意:上面創建“Java對象”字符串對象時,不要使用String str = "Java對象";代碼,因爲系統不會緩存這個字符串常量(會使用強引用來引用它),系統則不會回收被緩存的字符串常量。
       弱引用具有很大的不確定性,因此每次垃圾回收機制執行時都會回收弱引用所引用的對象,而垃圾回收機制的運行又不受程序員的控制,因此程序獲取弱引用所引用的Java對象時必須小心空指針異常。

1、5 虛引用

        軟引用和弱引用可以單獨使用,但虛引用不能單獨使用,必須和引用隊列聯合使用。單獨使用虛引用沒有太大的意義。虛引用的主要作用就是跟蹤對象被垃圾回收的狀態,程序可以通過檢查與虛引用關聯的引用隊列中是否已經包含指定的虛引用,從而瞭解虛引用所引用的對象是否即將被回收
       引用隊列由java.lang.ref.ReferenceQueue類表示,它用於保存被回收後對象的引用。當把軟引用、弱引用和引用隊列聯合使用時,系統回收被引用的對象之後,將會把被回收對象對應的引用添加到關聯的引用隊列中。與軟引用和弱引用不同的是,虛引用在對象被釋放之前,將把它對應的虛引用添加到它關聯的引用隊列中,這使得可以在對象被回首之前採取行動。
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("Java對象");
		ReferenceQueue<String> rq = new ReferenceQueue<>();  //創建引用隊列
		PhantomReference<String> pr = new PhantomReference<String>(str, rq);  //創建虛引用
		str = null;
		System.out.println(pr.get());   //①
		System.gc();  //強制垃圾回收
		System.runFinalization();
		System.out.println(rq.poll() == pr);  //取出引用隊列中最先進入隊列中的引用於pr進行比較 ②
	}
}
輸出結果爲:
null
true
       因此係統無法通過虛引用來獲得被引用的對象,所以執行①出輸出null。當程序強制垃圾回收後,只有虛引用引用的字符串對象會被垃圾回收,當被引用的對象被回收後,對應的引用會被添加到關聯的引用隊列中,因此②執行後會輸出true。

總結:使用這些引用類可以避免在程序執行期間將對象留在內存中。如果以軟引用、弱引用或虛引用的方法引用對象,垃圾回收器就能夠隨意地釋放對象。如果希望儘可能減小程序在其生命週期中所佔用的內存大小,這些引用類就很有好處。最後需要指出的是,要使用這些特殊的引用類,就不能保留對對象的強引用。如果保留了對對象的強引用,就會浪費這些類所提供的任何好處。




發佈了46 篇原創文章 · 獲贊 44 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章