JVM垃圾收集算法

垃圾收集器(Garbae Collection,GC),從誕生起就被要求完成3件事件:
1. 哪些內存需要回收?
2. 什麼時候回收?
3. 如何回收?

一、哪些內存需要回收?

在堆裏存放着Java世界中幾乎所有的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要確定這些對象之中哪些還“存活”着,哪些已經“死去”(即不可能再被任何途徑使用的對象)。
1 . 引用計數算法

給對象添加一個引用計數器,每當被一個地方引用時,計數器+1,當引用失效時,計數順-1,當計數器爲0時,表示對象不被引用。
客觀來講,引用計數算法簡單高效,但因爲它很難解決對象宰相互循環引用的問題,導致無法被主流Java虛擬機所使用。

2 . 可達性分析算法

通過一系列的稱爲“GC Roots”的對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到GC Roots 沒有任何引用鏈相連時,則證明對象 不可用的。

可作爲GC Roots的對象有以下幾種:

1.虛擬機棧(棧幀中的本地變量表)中引用的對象。
2.方法區中類靜態屬性引用的對象
3.方法區中常量引用的對象。
4.本地方法棧中JNI(即一般說的Native方法)引用的對象。

3 .對象引用
對象引用分爲以下四類:

1.強引用,就是程序代碼之中普遍存在的,類似”Object obj = new Object()”這類的引用。
2.軟引用,用來描述一些還有用但並非必需的對象,對軟引用關聯着的對象,在系統將要發生內存溢出異常之前,將會把這些對象列進回收範圍之中進行第二次回收。可使用SoftReference來實現軟引用
3.弱引用,用來描述非必需對象的,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。可使用WeakReference類來實現弱引用。
4.虛引用,也稱爲幽靈引用或者幻影引用,一個對象是否有虛引用的存在,完全不會對其生存無關緊要構成影響,也無法通過虛來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收地收到一個系統通知。可用PhantomReference來實現虛引用。

4 .對象的自救

在可達性分析算法中不可達的對象,也並非是”非死不可的“,要真正宣告一個對象死亡,至少要經歷兩次標記過程。如果對象 可達性分析後發現沒有與GC Roots相連接的引用鏈,那它會被第一次標記並且進行一次篩選,篩選的條件是該對象是否覆蓋了finalize()方法,若對象覆蓋了該方法並且尚未被調用過,那麼該對象會被放置在一個叫F-Queue的隊列之中,並在稍後由一個虛擬機自建的、低優先級的Finalizer線程去執行它。如果對象要拯救自己——只要重新與引用鏈上的任何對象建立關聯即可。如果對象此時還未建立關聯,那麼基本就被回收 。

5 .回收方法區(永久代)

永久代的垃圾收集 要回收兩部分內容:廢棄常量和無用的類。
1.廢棄對象,與回收java堆中的對象類似,但常量不再被有效引用時,將會被回收。
2.無用的類,通過滿足三個條件來確定一個類是否沒有用了。
2.1該在所有的實例都已經被回收了;
2.2加載該在的ClassLoader已經被回收了;
2.3該在對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。


二、垃圾收集算法

1.標記——清理算法
最基礎的收集算法“標記——清理”算法分爲“標記”和“清理”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象。
這裏寫圖片描述
它有兩個不足之處:

1.效率問題,標記和清除兩個過程效率都不高;
2.空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能導致分配大對象時,無法找到足夠連續空間而觸發一次GC。

2.複製算法
爲了解決效率問題,一種稱爲“複製(Copying)的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中一塊。當這一塊的內存用完了,就將還存活的對象 複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配進也就不用考慮內存碎片等複雜情況。只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是種算法的代價是將內存縮小爲了原來的一半。
這裏寫圖片描述

現在商業虛擬機都採用這種收集算法來回收新生代,IBM公司研究表明,新生代中對象98%是“朝生夕死”,所以並不需要1:1的比例爲劃分空間,而是將內存分爲一塊較大的Eden空間和兩個較小的Survivor空間,每次使用其中一塊Survivor。回收地,將存活的對象複製到另一塊Survivor空間中。HotSpot默認Eden和Suvivor大小比例爲8:1。另外這種方式採用了使用老年代內存“分配擔保”機制,當10%的Survivor空間不足以存放存活對象時,從老年代分配空間。

它的優缺點:

1.實現簡單,運行高效;
2.浪費最多50%空間,否則需要額外空間進行分配擔保;
3.因爲需要額外空間時行擔保,以應對100%存活的情況,老年代一般不能直接選用這種算法。

3.標記——整理算法
標記整理算法標記過程與“標記清除”一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存,從而不會產生空間碎片。
這裏寫圖片描述

3.分代收集算法
當前商業虛擬機的垃圾收集都採用“分代收集”算法,根據對象存活的週期不同將內存劃分爲幾塊,一般是把JAVA堆分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。

在新生代中每次回收都大批對象死去,所以選用複製算法。而老年代中因爲對象存活率高,沒有額外空間對它進行擔保,就必須使用“標記——清理”或“標記——整理”來進行回收。

三、HotSpot的算法實現

1.枚舉根節點
即進行可達性分析:可作爲GC Roots的節點主要在全局性的引用(常量或類靜態屬性)與執行上下文(棧幀中的本地變量表)中。該操作會暫停用戶線程(Stop The World)。在HotSpot中爲了避免逐個檢查類引用,使用了組稱爲OopMap的數據結構,在類加載完成時,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,這樣GC在掃描時就可以直接得知這些信息。

2.安全點
在OopMap的協助下可以很快完成GC Roots枚舉,但導致引用關係變化的情況很多,但不可能爲每條指令生成OopMap,那將會增加額外空間。爲解決該問題,HotSpot纔有了“安全點”的概念,即:只在“特定的位置”纔會將信息記錄以OopMap中,這個特定位置被稱爲“安全點”。當GC 發生需要暫停線程時,並非馬上暫停,而是得等線程運行到“安全點”時纔會暫停。

1.搶先式中斷,在GC 發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不是安全點,就恢復線程,讓它跑到安全點。
2.主動式,當GC時,設置一個標記,各線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起。輪詢標誌的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。

3.安全區域

安全區域可以理解爲安全點的放大,指在某代碼片段之中,引用關係不會發生變化。在這個區域任意地方GC都是安全的。

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