一、垃圾收集器判斷對象已死的算法
1.對象已死嗎?
判斷方法有以下兩種:
-
1.引用計數法
給對象添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器數值就減1;任何時刻計數器為0的對象就是不可能再被使用的。
但是這種方法有個弊端:很難解決對象之間相互循環引用的問題。 -
2.可達性分析算法
當一個對象到 GC Roots 沒有任何引用鏈相連(用圖論的話來說就是從 GC Roots 到這個對象不可達時),證明這個對象是不可用的。
-
3.四種引用類型
強引用:
Object obj = new Object()
,這就是強引用。只要強引用還存在,垃圾收集器永遠不會回收掉被引用的對象。軟引用:軟引用用來描述一些還有用但并非必需的對象。對于軟引用關聯的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。使用
SoftRefrence類
實現軟引用。弱引用:弱引用也是用來描述非必須對象的,但是強度比軟引用更弱。被弱引用關聯的對象只能生存到下一次垃圾回收之前。垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。使用
WeakRefrence類
實現弱引用。虛引用:這是最弱的一種引用關系。對象的生存時間與虛引用完全沒有關系,無法通過虛引用獲得對象實例。虛引用的唯一作用就是在對象被垃圾回收時收到一個系統通知。使用
PhantomRefrence類
實現虛引用。
二、垃圾收集算法
-
1.標記-清除算法(
Mark-Sweep
)最基礎的收集算法是“標記-清除算法”。這個算法分為兩個階段:標記和清除。
標記:在標記階段,collector
從mutator根
對象開始進行遍歷,對從mutator根
對象可以訪問到的對象都打上一個標識,一般是在對象的header
中,將其記錄為可達對象。
清除:在清除階段,collector
對堆內存(heap memory)
從頭到尾進行線性的遍歷,通過讀取對象的 header
信息,如果發現某個對象沒有標記為可達對象,則就將其回收。標記為可達對象的對象,將重新去掉 header
信息的標志位,以準備下次垃圾回收。
但是這種方法有2個不足:
1.效率問題,標記和清除兩個過程效率都不高。
2.空間問題,標記清除之后會產生大量的不連續的內存碎片,空 間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾回收動作。
-
2.復制算法 (Copying)
復制算法將內存劃分為兩個區間,在任意時間點,所有動態分配的對象都只能分配在其中一個區間(稱為活動區間),而另外一個區間(稱為空閑區間)則是空閑的。
當有效內存空間耗盡時,JVM 將暫停程序運行,開啟復制算法 GC 線程。接下來 GC 線程會將活動區間內的存活對象,全部復制到空閑區間,且嚴格按照內存地址依次排列,與此同時,GC線程將更新存活對象的內存引用地址指向新的內存地址。
而此時,其實空閑區間和活動區間已經進行了交換,垃圾對象已經全部留在了原來的活動區間,也就是現在的空閑區間。事實上,在活動區間轉換為空間區間的同時,垃圾對象已經被一次性全部回收。
優點:每次內存回收都是對整個半區進行內存回收,內存分配時不用考慮內存碎片的問題,只要移動堆頂指針,按照順序分配內存即可,實現簡單,運行高效。
缺點:1.代價是將內存縮小為原來的一半;2.如果對象的存活率很高,我們可以極端一點,假設是100%存活,那么我們需要將所有對象都復制一遍,并將所有引用地址重置一遍。復制這一工作所花費的時間,在對象存活率達到一定程度時,將會變的不可忽視。
復制算法比較適合的場景:對象的存活率要非常低才行,而且最重要的是,我們必須要克服50%內存的浪費。
下面看看復制算法回收前和回收后的內存模型:
回收前:
回收前回收后:
回收后由于 1,4 對象不可達,被回收掉,剩下的對象按照順序分配內存。這時右邊的區域變成了活動區間,左邊變成空閑區間,下次垃圾回收將再次調換回來。
-
3.標記-整理算法 (Mark-Compact)
標記-整理算法與標記-清除算法非常相似,它也是分為兩個階段:標記和整理。
標記:它的第一個階段與標記-清除算法是一模一樣的,均是遍歷 GC Roots,然后將存活的對象標記。
整理:移動所有存活的對象,且按照內存地址次序依次排列,然后將末端內存地址以后的內存全部回收。因此,第二階段才稱為整理階段。
標記-整理算法 GC 前后的圖示與復制算法的圖非常相似,只是沒有了活動區間和空閑區間的區別,而過程又與標記-清除算法非常相似。
回收前
回收前標記
回收前標記為 1 表示對象仍然可達。
整理
回收前可見,1,4 對象被回收了,剩下的存活對象的內存按照順序排列,這點很像復制算法,回收后不存在內存碎片問題,而且避免了內存減半的高額代價。但是處理過程又想標記-清除算法,不僅要標記所有存活對象,還要整理所有存活對象的引用地址。效率也不高,在效率上要低于復制算法。
參考資料