深入理解Java虛擬機---(4)對象是否“死亡”的判斷和GC的相關收集算法

寫在前面:

    在總結GC之前,首先要說,Java語言的一大好處就是講程序員從繁雜的垃圾回收,釋放對象內存空間中解放出來,相比之下,C++語言,還需要通過程序員手動的去管理,釋放內存空間,省去了程序員的一大部分工作。在這一篇博客中,將會總結對象的“死亡”判斷和GC的相關收集算法等。

GC的研究範圍:

    首先在前一篇博客中,我們知道了JVM的內存區域劃分,很多區域是線程獨享的,比如:程序計數器、虛擬機棧、本地方法棧,這幾塊區域的內存空間隨着線程產生而產生,隨着線程毀滅而毀滅,並且這一部分的內存大小,在類結構確定時,就已經確定,所以,這一部分的垃圾回收並不是我們的主要討論範圍。一般方法結束或者線程結束時,內存就跟着回收了。

    那麼另一部分,也就是所有線程共享的部分,比如Java堆和方法區,這一部分,我們只有在程序運行時,纔會知道創建了哪些對象,所以就意味着這部分的內存空間分配是動態的,是我們的主要討論範圍。

怎麼判斷對象的"生存"還是"死亡"?

    對象的"死亡"即一個對象不被任何對象引用了。判斷的對象的死亡的算法一般有兩種:

    (1) 引用計數法

    這種算法理解起來比較容易,就是給對象添加一個計數器,當一個地方引用時,計數器+1,同理,當引用失效的時候,計數器-1,當計數器=0的時候,對象就是相當於"死亡"。

    注:這種看起來很簡單的算法,雖然在一些公司有着不錯的應用,但是引用計數法無法解決對象循環引用的情況,如下面的代碼:

package gc;

public class GCDemo1 {
	public Object instance = null;

	public static void testGC() {
		GCDemo1 temp1 = new GCDemo1();
		GCDemo1 temp2 = new GCDemo1();
		// 對象之間循環引用
		temp1.instance = temp2;
		temp2.instance = temp1;
		System.gc();
	}
}
    上面的代碼中就是對象之間的循環引用,這樣計數器永遠不會爲0,但是這些對象實際上卻已經不會被訪問,GC也無法回收他們。

    

    (2) 可達性分析法(Reachability Analysis)

    算法的思路是,首先會找到程序中一系列稱爲"GC Roots"的對象作爲起點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到GC Roots沒有任何引用鏈相連,則說明GC Roots到這個對象是不可達的,也就是對象是無用的。就可以判定成GC 的回收對象。

    判定的GC Roots的標準:

--虛擬機棧中引用的對象

--方法區中類靜態屬性的引用的對象

--方法區中常量引用的對象

--本地方法棧(Native)引用的對象

引用的類型

    在JDK1.2以前,Java的引用定義爲如果reference類型的數值代表另一塊內存的起始地址,則說明是引用,反之則不是。在這樣的定義下,一個對象只有兩種可能:引用/非引用。這樣的標準我們無法描述特定的情況,比如說:當內存空間足夠的時候,我們像保留這些對象,內存空間在一次GC之後,還是很緊張,則我們清楚這些對象。

    在JDK1.2之後,對引用進行了補充,按引用的由強到弱排列爲:

    強引用(Stong Reference):程序中正常存在的引用,eg:Object obj = new Object(),強引用還存在,GC就不會回收掉強引用的對象。

    軟引用(Soft Reference):是指一些還有用但是並非必需的對象。在系統將發生內存溢出異常之前,將把這些對象列爲垃圾會回收器的回收對象進行第二次回收,如果這次回收之後,還沒有足夠的內存,纔會拋出內存溢出異常。

    弱引用(Weak Reference):描述非必需對象。但是強度弱與軟引用,被弱引用關聯的對象只能生存都下一次垃圾回收之前。無論當前內存足夠,都會回收掉只被弱引用的關聯的對象。

    虛引用(Phantom Reference):

    是最弱的一種引用關係。虛引用的存在不會影響其生存時間。爲對象設置虛引用的唯一目的是能在對象唄收集器收集的時候,對對象能收到一個系統通知。

對象的標記

    我們之前分析的可達性分析算法,而即使是可達性分析算法中不可達的對象,也並非是"非死不可"。要想讓一個對象真正死亡,將會經歷兩次標記過程。

    第一次標記:即在可達性分析之後,發現沒有與GC Roots想連接的引用鏈,,那麼將會被第一次標記,並且進行一次篩選,篩選的條件是此對象是否有必要finalize方法。如果對象沒有覆蓋finalize方法或者finalize方法以及被JVM調用過一次,那麼就會被判斷爲"沒有必要執行"。

    如果被判斷爲有必要執行finalize方法,那麼對象會被放在F-Queue隊列中,並稍後由一個由JVM自己建立的,低優先級的Finalizer線程區執行。執行意味着JVM會調用這個方法,但是不代表JVM會允許finalize方法運行結束(JVM這樣做是擔心finalize方法執行緩慢或者進入死循環,這樣會在F-Queue隊列中一直出不去)。

    第二次標記:GC將會對F-Queue中的對象進行第二次小規模的標記,如果對象在finalize方法中"拯救"了自己,即重新與引用鏈上的任何一個對象建立關聯即可。這樣,在第二次標記時,會被移除隊列。

注:finalize方法只會對系統調用一次,也就是說只可以被拯救一次,如果第二次再進入F-Queue隊列,就不會調用了finalize方法了。並且,finalize方法只是當初Java語言爲了讓程序員適應C++,我們並不建議使用finalize,因爲finally也可以實現finalize方法的效果。

方法區的垃圾回收:

    方法區是HotSpot虛擬機中的永生代,對於永生代和新生代來說,永生代的垃圾收集效率很低,而新生代的垃圾收集一般回收70%-95%的空間。

    永生代的垃圾收集主要分爲兩部分:廢棄常量+無用的類。廢棄常量的回收類似於堆的對象回收,比如:常量池中的字符串

"abcd",如果沒有任何一個對象引用"abcd"這個常量,那麼在GC的時候,"abcd"就會被回收。

    回收無用的類需要滿足以下3個條件:

    (1) 該類所有的實例都已經被回收,也就是Java堆不存在該類的任何實例。

    (2) 加載該類的ClassLoader已經被回收。

    (3) 該類對應的Java.lang.Class對象沒有被任何地方引用,也無法通過任何地方反射訪問該類的方法。

    注:在這些情況都滿足下,也僅僅代表無用類可以被GC回收,而不是必須被回收。

垃圾收集相關算法:

    在上述內容中,介紹了垃圾什麼時候會被回收,接下來,要結束回收垃圾時的原理。

    (1) 標記-清除算法(Mark-Sweep):

    標記-清除算法是最基礎的垃圾回收算法。算法分爲兩個階段:"標記階段"+"清除階段"。首先會標記出所有需要回收的對象(標記方式參考前面的內容),在標記過所有的對象之後,統一回收。

    缺點:這種垃圾回收算法,第一效率不高,因爲"標記階段"+"清除階段"的效率都不高。第二點,清除之後會產生大量不連續的內存碎片,導致如果向分配給一個對象較大內存空間時,找不到一塊連續的內存空間。

    (2) 複製算法(Copying):

    複製算法將內容按照容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當一塊的內存用完了,就將還存活的着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。

    這種算法,每次只對整個半區進行內存回收,就不用考慮內存碎片等情況。但代價是,每次犧牲了一半的內存。

    還有一種分配是將內存分爲3塊,一塊Eden和兩塊Survivor空間,每次使用一塊Eden和一塊Survivor空間,回收的時候,將Eden和Survivor還存活的對象一次性的複製到另外一塊空間上。Eden和Survivor是8比1(8:1)。這樣每次只有10%的空間被浪費。但是當Survivor空間不夠用時,需要其他內存進行分配擔保。

    (3) 標記-整理算法(Mark-Compact):

    複製收集算法在對象存活率較高時就要進行較多的複製操作,效率很低,避免出現對象100%存活的情況,所以老年代一般不採用這樣的算法。

    所以根據老年代的特點,提出了標記-整理算法,這種算法標記與標記-清楚算法一樣,但是這種算法是讓存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。

    (4) 分代收集算法(Generational Collection):

    是當前商業虛擬機都採用的算法。根據對象存活週期的不同將內存劃分爲幾塊。一般分爲新生代和老年代。新生代對象採取複製算法。老年代採取"標記-清楚"/"標記-整理"算法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章