Java對象的"後事處理"——垃圾回收(一)

一、Dead Or Alive

  咱們都知道對象死亡的時候須要進行垃圾回收來回收這些對象從而釋放空間,那麼什麼樣的對象算是死亡呢,有哪些方法能夠找出內存中的死亡對象呢?通常來講,咱們能夠這樣認爲:若是內存中不存在對當前對象的引用,則此對象必定是死亡狀態;可是死亡狀態的對象並不必定沒有其餘對象進行引用(可能存在死亡對象循環引用的狀況)。這裏須要說明一下,死亡的對象並不必定會被回收釋放佔用的空間,這種狀況就是常稱的"內存泄漏"。斷定對象存活的算法通常是如下兩種。算法

1.1 引用計數法

  引用計數法,即在對象內放置一個變量來表示這個對象被引用的次數,若是其餘對象引用了當前對象則變量值+1,若是失去引用則-1,當變量值爲0的時候表示沒有引用,應當回收。此算法並無被Java採用,由於其存在着一個致命的問題——循環引用。安全

 

  如上圖中,棧中沒有任何堆中兩個對象的引用,而堆中的兩個對象則互相持有對方的引用,若是使用引用計數法的話引用變量值永遠不會爲0,從而形成內存泄漏,兩個互相引用的對象沒法釋放空間。數據結構

public class TestForGc {

    TestForGc testInstance;

    // 模擬上圖的現象
    public static void main(String[] args) {
        TestForGc testA = new TestForGc();
        TestForGc testB = new TestForGc();
        testA.testInstance = testB;
        testB.testInstance = testA;
        testA = null;
        testB = null;

        // 建議垃圾回收器進行回收操做
        System.gc();
    }
}   

而後設置-XX:+PrintGCDetails打印GC日誌:

    最終新生代的對象所有被回收,說明JVM使用的並非使用引用計數法來實現垃圾回收。併發

 

1.2 可達性分析算法(GCRoots)

  GCRoots,大意爲選中一些特定的對象做爲根節點,而後從這些根節點出發尋找能夠引用到的全部對象,造成一條引用鏈引用網),不在這條鏈中的對象則標記爲死亡,進行回收。根節點的特定對象從下列四種產生:spa

  一、虛擬機棧中引用的對象。線程

  二、本地方法棧中引用的對象。3d

  三、方法區中靜態變量引用的對象。日誌

  四、方法區中常量引用的對象。code

   使用GCRoots便不會出現循環引用的問題,如圖,雖然A、B相互引用,可是因爲不在根節點的引用鏈中,因此會被標記爲可回收對象。對象

   在Hotspot虛擬機對GCRoots算法的實現中,大體能夠分爲三個部分理解。

  1.2.1 枚舉根節點

  如上所說,根節點的選取對象有四處,若是虛擬機對這些位置進行全盤掃描的話,效率天然要影響很多,因此Hotspot採用一種數據結構——OopMap來解決這個問題。在類加載完成的時候,虛擬機將對象的什麼偏移量有什麼對象計算出來,在JIT編譯過程當中在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣一來GC在掃描的時候就能夠直接獲得這些引用的信息,從而減小GC的停頓時間。順便一提,在枚舉根節點的時候,爲了保持「一致性」,不能再掃描的時候還出現對象引用變化的狀況,因此須要暫停全部Java執行線程(被稱爲"STOP-THE-WORLD"),即使在具備劃時代意義、能夠併發執行的CMS收集器中在枚舉根節點的時候也須要STW。

  1.2.2 安全點的設置

  OopMap數據結構能夠說爲GC的掃描減小了很多的時間,可是隨之而來的還有一個問題,若是每條指令都生成對應的OopMap,那麼想必須要大量的額外空間,GC的空間成本將十分巨大,就是什麼時候生成對應OopMap成爲當前面臨的問題。以前說過在特定的位置會記錄下引用的位置,這個特定的位置就是OopMap的生成時機,也就是「安全點(SafePoint)」,在Sop-The-World的時候線程要先跑到安全點才能夠進行線程的停頓。那該如何判斷這個特定的位置呢?若是設置的太少可能會致使GC時間變長,設置的太多會增大運行時的負荷。Hotspot給出的答案是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定。"長時間執行"的明顯特徵就是指令複用,例如方法調用、循環跳轉、異常處理等,只有這些指令才能產生安全點。

  對於安全點來講,另一個問題就是採用什麼樣的方式讓全部的線程跑到最近的安全點停頓。有兩種實現的方式:

  一、搶先式中斷:在GC發生的時候首先暫停全部線程,若是發現有線程沒在安全點的話,則恢復線程,讓其跑到最近的安全點再進行暫停。如今已經不多有使用搶先式的了。

  二、主動式中斷:GC發生的時候不強制暫停線程,而是設置一個標識變量,線程會去輪詢這個標誌,若是爲true則將本身中斷掛起。這個輪詢的位置和安全點是重合的,還有建立對象時須要分配內存的地方。

  1.2.3 安全區域

  上面安全點的設置幾乎已經解決了問題,可是還少了一點,就是創建在線程都是執行狀態的時候,那線程不執行的時候呢,例如進入休眠狀態的線程,這時候本身不能跑到安全點也不能等待JVM分配時間。此時就須要安全區域來解決這一點。

  安全區域指的是在一段代碼塊中,引用關係不會發生變化。當程序走到安全區域的時候,則標識當前線程進入了安全區域。這時候發生GC的時候則能夠不用管有安全區域標識的線程,而這些線程在快離開安全區域的時候必需要檢查是否完成了根節點的枚舉或者整個GC的過程),若是完成了才能夠離開安全區域,不然必須待到完成爲止。

二、垃圾回收算法

  如今咱們知道哪些對象是死亡的,哪些對象應該回收,而這個回收有許多種實現的方式(算法),有的算法對死亡對象進行標記最後一併清除、有的算法將內存分塊而後將存活對象從一頭搬到另外一頭,還有算法在清除完死亡對象貼心的將存活的對象整放在一起,這些都是咱們接下來要說的。

2.1 標記-清除算法

  正如這個算法的名稱通常,其總共有兩個階段——"標記"和"清除":首先其會對全部的死亡對象進行標記,最後再一塊兒將這些對象回收。

  這個算法是基礎的算法,後續的算法都是對其缺點的一些改進。此算法有兩個不足的地方,其一從上圖也能夠看得出來,垃圾回收後的內存空間不連續,形成許多的內存碎片。其二就是其效率問題,標記和清除的效率並非過高,因此後續出現的算法都對兩個缺點的調整和改進。

 

2.2 複製算法

  爲了解決效率內存碎片的問題,一種稱做"copy"的算法出現,這個算法將內存空間分紅兩份或以上,一份存放對象,一份空白,當進行垃圾回收的時候將全部的存活對象複製到空白的一份中,而後清空以前存放對象的空間

  上圖也能夠看得出,對此算法最重要的是內存空間的切分,若是切分不當可能會浪費大量的空間。固然使用得當也有十分值得的優勢:必定範圍內的高效率沒有內存碎片。

 缺點:

  一、適用於存活對象相較死亡對象少的狀況,例如新生代,若是存活的對象較多的話可能獲得相反的效果。因此才說是必定範圍的高效率。

  二、須要劃份內存空間。若是自己的內存空間比較小還去劃分的話那可能會致使頻繁的GC,停頓時間增多,影響用戶體驗。

  另:此算法通常用在新生代作垃圾回收算法,而且將新生代分紅三個部分——兩個Survivor和一個Eden區,其比例默認爲1:1:8(能夠經過虛擬機參數改變)。當咱們生成一個對象(經過關鍵字new或者反射)的時候,對象首先會分配在Eden區,等到Eden區放不下的時候則觸發一次MinorGC,將Eden和其中一個Survivor中的存活對象一塊兒移到另外一個Survivor中,而後清空。順帶一提,有存活對象的Survivor老是稱做From區,空白的Suvivor老是稱做To區,通常新生代存活對象佔5%左右。

 

2.3 標記-整理法

  複製算法是一個很是優秀的算法,但其只在存活對象想多較少的狀況下表現良好,而對於其餘例如年老代中這些存活對象較多的區域則多是一種糟糕的選擇。因此,須要一個更加合適的算法——標記-整理法。這個算法原理和步驟基本跟標記-清除同樣,可是多出一個整理的步驟,也就是說整個過程爲標記-清除-整理,結束以後不會產生內存碎片

2.4 分代算法

  嚴格來講這不能算是一種算法,應該是一種理念。其把整個內存空間分爲兩個區域——新生代年老代(1.8以前還有一個永久代,也就是方法區,可是在1.8以後已經刪除)。而且虛擬機對對象定義了年齡的概念,表示該對象熬過了多少次GC,以此來做爲對象放在新生代仍是年老代的標準之一,默認新生代的對象15歲以後就能夠進入年老代了。對於兩個區域採用的回收算法也是不一樣的,新生代通常採用複製算法,年老代通常採用標記-整理法,固然具體仍是得看使用的垃圾回收器,若是年老代使用的是CMS的話那麼就是標記-清除了。

 

It is an honor if I could get some advices or corrections from you guys.

相關文章
相關標籤/搜索