1. 程式人生 > >深入理解JVM(三)——垃圾收集器

深入理解JVM(三)——垃圾收集器

需要了解GC嗎?

Q:需要了解GC和記憶體分配嗎?
A:當需要排查各種記憶體溢位,記憶體洩露問題時;當垃圾回收成為系統高併發的瓶頸時

哪些記憶體需要回收?

程式計數器,虛擬機器棧,本地方法棧隨著執行緒生而生,執行緒滅而滅,棧幀隨著方法的進入和退出而進棧和出棧。基本上類結構確定下來就已知了。
Java堆和方法區則不同,只有執行時才知道要建立哪些物件,這部分記憶體的分配和回收是動態的。

物件已死嗎?

  • 引用計數演算法

    給物件中新增一個引用計數器,每當有一個地方引用它,計算器加1;引用失效時,計算器減1。任何時刻計算器為0的物件就是不可能再被使用的。
    無法解決物件之間相互迴圈引用的問題

  • 可達性分析演算法

    通過一系列的稱為”GC Roots”的物件作為起點,從這些節點開始向下搜尋,搜尋走過的路徑稱為引用鏈,當一個物件到達”GC Roots”沒有任何引用鏈相連,則物件不可用。

    GC Roots物件包括

    • 虛擬機器棧(棧幀中的本地變量表)中引用的物件
    • 方法區中類靜態屬性引用的物件
    • 方法區中常量引用的物件
    • 本地方法棧中JNI(Native方法)引用的物件

    Java中引用的概念

    • 強引用,在程式程式碼中普遍存在,如new,只要強引用存在,垃圾收集器永遠不會回收掉引用的物件
    • 軟引用,描述一些還有用但並非必需的物件。對於軟引用關聯的物件,系統在將要發出記憶體溢位異常之前,將會把這些物件納入回收範圍中進行第二次回收。JDK提供SoftReference類來實現軟引用。
    • 弱引用,描述非必需的物件,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。JDK提供WeakReference類來實現弱引用。
    • 虛引用,一個物件是否有虛引用存在,完全不會對其生命週期產生影響,也無法通過虛引用取得一個例項。為物件設定虛引用的唯一目的是為了能在這個物件被垃圾回收時收到一個系統通知。JDK提供PhantomReference類來實現虛引用。

真正宣告一個物件的死亡,至少要經歷兩次標誌過程。第一次可達性分析後發現沒有與GC Roots連線的引用鏈,那麼它會被第一次標記並且進行一次篩選,條件為物件是否有必要執行finalize()方法。(物件沒有覆蓋該方法或者該方法已呼叫,則虛擬機器視為沒必要執行)
有必要執行finalize則放在F-Queue中,並由虛擬機器稍後建立低優先順序的執行緒去執行。保證觸發這個方法但是不保證等待它執行結束。(防止一個方法執行緩慢,死迴圈,阻塞佇列)
稍後GC會對F-Queue中的物件進行第二次標記,也是物件逃脫GC的最後一次計劃,可以把自己(this)賦值給某個類的變數就可以重新與引用鏈上的物件關聯了。

回收方法區

HotSpot中的永久代,價效比較低。在堆中,特別是新生代,一次垃圾回收可以回收70%-95%的空間。
永久代的垃圾收集遠低於此,永久代的垃圾收集包括廢棄的常量和無用的類。

  • 廢棄的常量
    與回收Java堆中的物件非常相似,將入字串”abc”已經進入常量池,而沒有任何String物件引用常量池中的”abc”常量,也沒有其它地方引用這個字面量,此時GC,該常量會被清理出常量池。
  • 無用的類
    有三個條件,該類的所有例項都已經被回收,載入該類的ClassLoader已經被回收,該類對應的java.lang.class物件沒有在任何地方別引用,無法在任何地方通過反射訪問該類的方法。

垃圾收集演算法

  • 標記-清除演算法

    首先標記出所有要回收的物件,在標記完成後統一回收所有被標記的物件。存在兩個問題,效率問題,標記和清除這兩個過程效率都不高;另一個是空間問題,產生大量的不連續的記憶體碎片。

  • 複製演算法

    將記憶體按照容量劃分成大小相等的兩塊,每次只使用其中的一塊,用完之後將存活的物件複製到另一塊上,然後將已使用過的記憶體空間一次清理掉。實現簡單,執行高效,只是將記憶體縮小為原來的一半了。

  • 標記-整理演算法

    複製收集演算法在物件存活較高時就要進行多的複製操作,效率會下降。標記-整理演算法標記過程依然按照之前的標記,但是不是直接對可回收物件進行清理,而是所有存活的物件向一端移動,然後直接清理掉端邊界以外的記憶體。

  • 分代收集演算法
    根據物件存活週期的不同將記憶體劃分為幾塊。一般Java堆分為新生代和老年代,根據特點採用最適當的收集演算法。新生代大多朝生夕死,採集複製演算法;老年代物件存活效率高,採用標記-清除或者標記-整理演算法進行回收。

HotSpot演算法實現

stop the world

GC Roots即全域性性引用(常量及類靜態屬性),與執行上下文(棧幀中的本地變量表),進行可達性分析時,會造成停頓。因為必須保持一致性,不能出現分析過程中物件引用關係還在不斷的變化,即stop the world。即使CMS收集器中,列舉根節點也是必須要停頓的。

OopMap

HotSpot用一組稱為OopMap的資料結構,代替跟節點的列舉。在類載入完成的時候,將物件內什麼偏移量上是什麼型別計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和暫存器中的哪些位置引用,這樣GC掃描時就可以直接知道這些資訊啦。

SafePoint

並不是每一條指令都生成OopMap,上文已經說過只有到“特定的位置”才記錄這些資訊,這些位置稱為安全點(SafePoint),也就是說程式不是所有的地方都能停止下來GC,而是隻有到了安全點才能GC。
安全點的選擇以程式是否具備長時間執行的特徵為標準選定,因為指令執行的時間非常短,長時間執行明顯的特徵就是指令序列複用,即方法呼叫,迴圈跳轉,異常跳轉等。

多執行緒與SafePoint

如果保證GC時所有執行緒都達到SafePoint呢?

  • 搶先式中斷

    GC發生時,不需要執行緒配合,首先將所有執行緒全部中斷,如果發現有執行緒中斷的地方不在SafePoint,則恢復執行緒,讓其執行到SafePoint。(該方式極少被JVM採用)

  • 主動式中斷

    GC需要中斷執行緒時,不直接對執行緒操作,僅僅簡單地設定一個標誌,各個執行緒執行時主動去輪循這個標誌,發現中斷標誌為true就自己中斷掛起。輪循的標誌的地方和SafePoint是重合的,另外加上建立物件需要分配記憶體的地方。

SafeRegion

SafePoint看似完美解決了進入GC的問題,但是如果有的執行緒不執行怎麼辦?執行緒處於sleep或者blocked狀態,沒有分配到CPU時間,那麼就無法響應中斷請求。
安全區域是指在一段程式碼片段中,引用關係不會發生變化,這個區域任何地方開始GC都是安全的。
執行緒執行到SafeRegion時,標誌自己已經進入SafeRegion,這時發生GC,就不用管狀態為SafeRegion的執行緒。線上程離開SafeRegion時,檢查JVM是否完成根節點的列舉,如果完成執行緒繼續執行,如果沒有則等待。