1. 程式人生 > >深入理解java虛擬機器之自動記憶體管理機制(二)

深入理解java虛擬機器之自動記憶體管理機制(二)

垃圾收集演算法

    java中的記憶體是交給虛擬機器管理的。要實現垃圾回收,必須考慮如下三個問題:

    1. 哪些記憶體需要回收?

    2. 什麼時候回收?

    3. 怎麼回收?

    對於第一點,往大了來說,是堆和方法區的記憶體需要回收。往具體了來說,是堆中哪些物件的記憶體可以回收了?方法區中哪些類的資訊的記憶體可以回收了?要解答這兩點問題,必須要有演算法能夠判斷
  哪些物件已“死”,哪些類的資訊不再需要。

    對於第二點,則要在效能與效率中做好兼顧,不能過於頻繁影響程式效能,也不能太少讓收集器太閒,積累太多垃圾。

    對於第三點,則要求我們想出演算法,實現收集器。然而,一般來說,需要根據不同型別物件的特點使用不同的收集演算法,比較通用的分類方法是將物件分為新生代與老年代,不同的代使用不同的演算法
  可以有效提升收集效率。

 

(一)物件已死嗎?

  判斷物件是否已經死亡,有兩種方法。

  一、引用計數演算法

      給物件新增一個引用計數器,每當有一個地方引用它時,計數器就+1,引用失效就-1。該方法簡單高效,但是解決不了物件之間相互迴圈引用的問題。

  二、可達性演算法分析

      該演算法是java虛擬機器主流演算法。基本思路是,通過一系列的稱為GC Roots的物件作為起始節點,然後通過這些節點向下搜尋,走過的路徑稱為引用鏈,如果某個物件到GC Roots沒有引用鏈,

    就可以說該物件已死。

      那麼哪些物件可以作為GC Roots呢?

      1. 虛擬機器棧中引用的物件

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

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

      4. Native方法中引用的物件

  三、再談引用

    引用的意思就是這塊記憶體裡儲存著別的記憶體的地址。java對引用進行了擴充。

    1. 強引用,顯式的建立物件就是強引用。

    2. 軟引用,在虛擬機器將要發生記憶體溢位的情況時,會回收該區域。

    3. 弱引用,引用的物件只能存活到下一次垃圾收集之前。

    4. 虛引用。

 

(二)方法區中的常量與類已死嗎?

  常量容易判斷,類則要求很高。需要滿足三個條件。

  1. 該類所有例項都被回收,即java堆中不存在該類物件。

  2. 載入該類的classloader已經被回收。

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

  

(三)垃圾蒐集演算法

  垃圾蒐集演算法主要有三種,其中一種適用於新生代,另外兩種適用於老年代。所謂的新生代就是剛建立的物件,老年代就是在多次垃圾收集中存活下來的物件。

  一、複製演算法

    因為在java程式中,許多物件都是“朝生夕死”,因此會有大量的垃圾存在。將記憶體區域邏輯上劃分為兩部分,每次分配物件使用其中一塊,另外一塊在垃圾收集時用來存放存活下來的物件,這樣可以
  減少記憶體碎片的產生。通常將新生代可用記憶體空間劃分為3部分,最大的是Eden,然後有兩個相同大小的Survivor。每次分配物件,就使用Eden和其中一塊Survivor。當垃圾收集時,把存活的物件複製到
  另外一塊Survivor上。

  二、標記-清除演算法

    分為兩個步驟,分別是標記和清除。缺點有兩個,其一,標記和清除兩個過程效率都不高;其二,會產生大量的記憶體碎片。

  三、標記-整理演算法

    將存活的物件往一端移動。可以解決記憶體碎片問題。

 

(四)HotSpot的演算法實現

  一、列舉根節點

    hotspot採用的是可達性分析演算法去判斷物件的存活。在進行可達性分析的時候,必須要stw(stop the world),即必須停止所有的java執行執行緒。hotspot中用到了OopMap的資料結構去儲存哪些地方
  存放著物件的引用。在類載入的時候,jvm就把物件內多少偏移量有引用計算出來,存放在物件的型別資訊裡。所以從物件開始向外的掃描可以是準確的。每個被JIT編譯過後的方法也會在一些特定的位置
  
記錄下OopMap,記錄了執行到該方法的某條指令的時候,棧上和暫存器裡哪些位置是引用。這樣GC在掃描棧的時候就會查詢這些OopMap就知道哪裡是引用了。這些特定的位置主要在: 
  1、迴圈的末尾 
  2、方法臨返回前 / 呼叫方法的call指令後 
  3、可能拋異常的位置 
  這種位置被稱為“安全點”(safepoint)

  

  二、安全點

    之所以要選擇一些特定的位置來記錄OopMap,是因為如果對每條指令(的位置)都記錄OopMap的話,這些記錄就會比較大,那麼空間開銷會顯得不值得。選用一些比較關鍵的點來記錄就能有效的縮
  小需要記錄的資料量,但仍然能達到區分引用的目的。在這些安全點位置,執行緒的狀態都已經可以被確定了,裡面的引用關係也已經基本確定,這時進行OopMap的記錄是比較好的。

    OopMap的記錄在編譯時在安全點的位置就開始記錄,GC在執行時在安全點的位置要開始GC,那麼該如何中斷執行緒去GC呢?這裡分為兩種中斷方式,分別是搶先式中斷和主動式中斷。

    搶先式中斷是等發起GC時,停止所有執行緒,當發現有執行緒沒到安全點時就恢復執行,幾乎不使用這種方法。

    主動式中斷是當需要GC時,不主動中斷執行緒,而是簡單設定一個標誌,執行緒執行到安全點位置和建立物件需要分配記憶體時自己主動去輪詢這個標誌,標誌為真就將自己掛起。

  三、安全區域

    安全區域是對安全點的補充。安全點是在Java程式執行過程中GC的一個節點,而程式不執行時,即sleep或者blocked時,就出問題了。安全區域則是專門解決這個問題的。
    安全區域是指在一段程式碼中,引用關係不會發生變化。當進入安全區域時,jvm發起GC就不用管這個執行緒了。當執行緒要離開區域時,必須要檢查是否已經完成根節點列舉,完成,則可以安全離開,未完成
  則必須停下等待,直至收到可以安全離開的訊號為止。