垃圾收集器與記憶體分配策略_垃圾收集演算法

z1340954953發表於2018-03-28

前面瞭解了java執行時區域的各個部分,其中程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅

;棧中的棧幀隨著方法的進入和退出執行者出棧和入棧操作。每個棧幀中分配多少記憶體基本是在類結構確定下來時就已知的。

因而這幾個區域的記憶體回收都具有確定性。

也就是在對於程式計數器、虛擬機器棧、本地方法棧這幾個記憶體區域不需要考慮回收問題,方法結束或執行緒銷燬,記憶體自然隨著回收了

java堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法的多個分支需要的記憶體也可能不一樣,只有在程式處於執行期間才能知道建立了那些物件,這部分記憶體的建立和回收都是動態的,垃圾收集器關注的是這部分記憶體

判斷物件是否存活

1. 引用計數演算法

很多教科書判斷物件是否存活的演算法,給物件中新增一個引用計數器,每當有一個地方引用它,計數器就加1,引用失效,計數器減1;任何時刻計數器為0的物件是不可能在被使用的

但是主流的java虛擬機器沒有采用計數器演算法,主要的原因是它很難解決物件鍵互相迴圈引用的問題。

public class ReferenceGc {
	class A{
		B b;
	}
	class B{
		A a;
	}
	public static void main(String[] args) {
		ReferenceGc rf = new ReferenceGc();
		A a = rf.new A();
		B b = rf.new B();
		a.b = b;
		b.a = a;
		a = null;
		b =null;
		System.gc();
	}
}

類A有個欄位是B類型別,類B有個欄位是A類的型別,建立物件a和物件b,此時引用a指向A物件,引用b執行B物件,後面在將兩個類的欄位相互引用,就算a,b引用失效,A類物件仍然被B類欄位引用著,A類和B類獨享都訪問不到了,但是引用計數卻不為0,無法通知到垃圾回收器回收.

2. 可達性分析演算法

主流的善用程式語言(Java)主流實現中,都是通過可達性判斷物件是否存活。

演算法的基本思想:通過一系列"GC Roots"的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連(就是從GC Roots到這個物件不可達)時,則證明此物件是不可用的。


可以作為GC Roots物件:

1> 虛擬機器棧(棧幀中的本地變數表) 中引用的物件

2> 方法區中類靜態屬性引用的物件

3> 方法區中常量引用的物件

4> 本地方法棧中JNI,即一般說的Native方法引用的物件。

就拿上面相互引用的例子來說:

A 類的欄位是B型別的,b物件通過A引用找A物件,A引用為null,認為A是不可達的,垃圾回收器回收A物件。

B 類的欄位是A型別的,a物件通過B引用找B物件,B引用為null,認為B是不可達的,垃圾回收期回收B物件。

再談引用

我們希望能描述這樣一類物件:當記憶體空間還足夠時,則能保留在記憶體中,如果記憶體空間在進行垃圾收集後還是非常緊張,則可以拋棄這些物件。

Java對引用的進行擴充,將引用分為強引用,軟引用,弱引用,虛引用,這4中引用強度依次減弱

1>  強引用就是指在程式程式碼之中普遍存在的,類似Object obj = new Object()這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。

2> 軟引用是用來描述一些還有用但並非必須的物件。對於軟引用關聯的物件,在系統將要發生記憶體溢位異常前,將會把這些物件進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。

3> 弱引用也是描述非必須物件的,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生前。

4> 最弱的一種引用關係,完全不會對物件的生存時間影響。

物件的生存還是死亡

即使在可達性分析演算法中不可達的物件,也並非是“非死不可”,一個物件真正銷燬,要經歷兩個階段,如果物件在進行可達性分析後沒有和GC Roots相連的引用鏈,會被第一次標記並進行一次篩選,篩選的條件是物件是否有必要執行finalize方法。當物件沒有覆蓋finalize()方法或者finalize()方法已經執行過,虛擬機器將這兩種情況視為 "沒有必要執行",直接銷燬

可以在finalize中逃脫物件銷燬,但是不建議這麼做。

回收方法區

很多人認為方法區(或者HotSpot虛擬機器中的永久代)是沒有垃圾回收的,java虛擬機器規範中確實說過可以不要求虛擬機器在方法區實現垃圾收集,而且在方法區中進行垃圾收集的價效比一般比較低;在堆中(按分代演算法分為新生代、老升代) ,新生代中,常規進行一次垃圾收集一般可以回收70%~95%的空間,永久代的垃圾收集效率遠低於此。

永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的物件非常相似。

1. 判斷一個常量是否是廢棄常量比較簡單,只要判斷沒有任何引用引用這個常量。

比如常量池中字串“abc”,如果沒有任何引用引用到這個常量,就會被系統清理出常量池,常量池中的其他類、方法、欄位的符號引用也類似

2. 判斷一個類是否是無用的類,需要同時滿足下面三個條件

1> 該類的所有例項都已經被回收,也就是Java對中存在該類的任何例項

2> 載入該類的ClassLoader已經被回收

3> 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法

垃圾收集演算法

1. 標記-清除演算法

通過可達性演算法標記處所有需要回收的物件,在標記完成後統計回收已死亡的物件

它的不足之處有兩個:一個是效率問題,標記和清除的效率都不高,另一個方面,標記清除後產生大量不連續的記憶體碎片,可能造成以後分配較大物件後,由於空間不足,會再次觸發一次垃圾回收


2. 複製演算法

將可用記憶體按容量劃分為大小相等的兩塊(一半,一半),每次只使用其中的一塊。當這一塊的記憶體用完,將還存活的物件複製到另外一塊上,將已使用的記憶體空間進行清理。


優點:這樣每次都是對半個記憶體區域進行回收,記憶體分配時在也不用考慮記憶體碎片等複雜情況,只要移動堆定指標,按順序分配記憶體,簡單,高效

缺點:將記憶體縮小為原來的一半,代價太高

現在商業虛擬機器都是採用這種收集演算法來回收新生代,IBM研究表明新生代中98%,是朝生夕死的,所以不需要按照1:1來劃分記憶體空間。一般是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor空間進行清理,將存活物件移動到另一塊Survivor空間(如果不夠用,會通過分配擔保機制移動到老年代中)

3. 標記-整理演算法

複製演算法在物件存活率較高時要進行較多的複製操作,效率會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件100%存活的極端情況,所以老年代不選用複製演算法

根據這個特點,提出來標記整理演算法,不過的是在可達性區分開存活物件和可回收物件後,將存活物件,移動到記憶體區域的邊界,然後清除邊界外的記憶體。


4. 分代收集演算法

當前商業虛擬機器的垃圾收集都是採用"分代收集"演算法,將物件存活週期的不同將記憶體劃分為幾塊,一般是劃分為新生代和老年代,然後根據不同年代的特定採用適當的收集演算法。

新生代:大量物件消亡,少量存活,採用複製演算法

老年代:存活率高,沒有額外的記憶體分配,採用標記-清除演算法,標記-整理演算法



相關文章