JVM垃圾回收

叫我劉三青發表於2019-04-05

Java 和 C++ 之間有一堵由記憶體動態分配和垃圾收集技術所圍成的"高強",牆外面的人想進去,牆裡面的人卻想出來 ————《深入理解Java虛擬機器》


文章,筆記,原始碼地址:github.com/leosanqing/…


Java 由於JVM自帶垃圾回收的機制,所以對於很多初中級的程式猿非常方便,不需要自己寫語句控制垃圾的回收,只需要關注業務邏輯即可。尤其是自己寫demo或者練手的專案,基本不用擔心垃圾回收的事情

但是,對於線上的專案,一旦出現垃圾回收的問題,往往是非常致命的。所以不論是校招還是社招基本都會問到JVM的垃圾回收機制以及演算法

這個文章的結構如下

  • 哪些記憶體需要回收
  • 什麼時候回收
  • 如何回收

哪些記憶體需要回收

主要分為兩個部分

  • 程式計數器,虛擬機器棧,本地方法棧
  • Java堆和方法區(基本回收這裡的)

如果你知道 JVM執行時資料區,那麼應該知道上面的兩部分剛好是按照執行緒共不共享來劃分的。這樣也方便記

如果不瞭解的話,可以參考我的這篇 JVM執行時資料區的文章

第一部分

剛剛也提到了,如果瞭解JVM執行時資料區的話,第一部分的執行緒是不共享的他們都隨著執行緒生而生,執行緒滅而滅

這三個區域的記憶體分配和回收都具備確定性,不必過多考慮回收問題,在方法結束或者執行緒結束時,記憶體自然而然就跟著回收了

第二部分

基本是執行時才已知,且執行緒共享,基本上考慮垃圾回收演算法以及回收判定都是這兩個部分。

方法區的回收

在方法區的回收垃圾的價效比一般是比較低的

在JDK1.7或者1.8之前,很多人將方法區和Hotspot中的永久代等同。其實二者也是有區別的前者是 JVM 的規範,而後者則是 JVM 規範的一種實現

**注意:**只有 Hotspot中才有永久代(PermGen space)的概念,其他的JVM比如Oracle的JRocket和IBM的J9並沒有這個概念。 而且永久代從JDK1.7之後就開始逐步移除了,1.8之後就已經完全移除,轉為元空間

具體可以參考這篇博文,永久代和元空間

他主要回收的內容是廢棄常量無用的類

廢棄常量

以回收常量池中的字面量為例,如果一個字串"abcd"已經加入了常量池,但是並沒有任何String物件引用常量池中的 "abcd"常量

常量池中其他類(介面)、方法、欄位的符號引用也與此類似

回收Java堆中的方式也與此類似

無用的類

判定無用的類條件比較嚴苛,需要同時滿足下列三個條件

  • 該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項
  • 載入該類的ClassLoader已經被回收
  • 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法

注意這裡僅僅是"可以",而並不是和物件一樣,不使用了就必然會回收

什麼時候回收

當然是物件已經"死掉"的時候,主要有以下兩種方法

  • 可達性分析(JVM使用)
  • 引用計數

引用計數法

思路

這個邏輯比較簡單

  • 給物件中新增一個引用計數器,
  • 每當有一個地方引用它時,計數器值就加1
  • 當引用失效時,計數器值就減1
  • 當計數器為0的物件就是不可能再被使用的

缺點

但是這個方法有一個缺點:互相引用的物件不會被回收

比如有下面程式碼(除此之外沒有其他引用,這兩個物件也不會被訪問,應該回收)

obj1.instance = obj2;
obj2.instance = obj1;
複製程式碼

對於使用 這種判定演算法的虛擬機器,並不會收到回收他們通知,就不會回收他們兩個

可達性分析法

思路

  • 通過一系列的稱為 "GC Root"的物件作為起始點
  • 從這些節點開始向下搜尋,搜尋走過的鏈稱為引用鏈
  • 當一個物件到 GC Root 沒有任何引用鏈相連,則證明此物件是不可用的(用圖論的話,就是GC Root到這個物件不可達)

可以作為GC Root的物件

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

即便是不可達的物件也不是立即進行回收,他會經歷兩次標記的過程

引用

不論是可達性分析演算法還是引用計數法,要判定物件是否"死亡",都需要根據引用來判定

在Java中一共有四種引用(JDK1.2之後),強度依次遞減

  • 強引用(一定不會回收)
  • 軟引用(記憶體不夠回收)
  • 弱引用(發生回收就會回收)
  • 虛引用()

強引用

Strong Reference在程式碼中普遍存在的,只要強引用存在,JVM就一定不會回收。

比如Object obj = new Object();

軟引用

Soft Reference發生記憶體溢位異常之前,將這些軟引用連線的物件列近回收範圍之中的第二次回收。如果這次回收之後還沒有足夠的記憶體,才會丟擲記憶體溢位異常

弱引用

Weak Reference只能活到下一次垃圾回收發生之前。無論記憶體是否足夠,都會將軟引用連線的物件回收

虛引用

Phantom Reference,他是最弱的一種引用關係。一個物件是否含有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項

"為一個物件設定虛引用關聯的唯一目的就是能在這個物件被回收時收到一個系統通知” ————《深入理解Java虛擬機器》

如果想多瞭解java四種引用的應用說明,可以參考下面這篇博文;

java四大引用的特點及應用場景

如何回收

垃圾收集演算法主要有四種:

  • 標記-清除
  • 標記-複製
  • 標記-整理
  • 分代收集

標記-清除

顧名思義,標記需要清除的物件然後清除。他是最最基礎的,之後的都是通過他改進的

JVM垃圾回收

缺點

  • 效率:標記和清除的效率都不高
  • 空間:清理後會產生大量的不連續的記憶體,之後分配大記憶體物件時,不得不提前觸發垃圾回收

標記-複製

過程

將記憶體分為兩塊,每次只使用其中一塊。每次垃圾回收時,將存活的物件複製到另一塊,然後清理那塊全部空間。

JVM垃圾回收

不足

  • 記憶體少了一半

比較適合可以大量進行垃圾回收的新生代

為了解決這個問題,JVM將新生代的 Eden區和其中一個survivor區域劃分比例調整為8:1.而survivor共有兩個,每次只用其中一個,另一個用來複制,存放活著的物件

標記-整理

過程

標記過程一樣,就是清除的時候,將所有活著的物件移到一端。然後清理剩下的區域

JVM垃圾回收

缺點

  • 回收效率不高。

所以一般用在老年代的回收

分代收集演算法

主流的虛擬機器都採用這種演算法

主要是根據不同代的特點,選擇相對合適的上述演算法。

比如Java新生代物件存活率比較低,有大批的物件死去,所以採用標記-複製演算法,而老年代的物件相對比較穩定,存活率較高而且物件較少,也沒有額外的空間對他進行分配擔保,所以就採用標記-整理演算法

Hotspot中演算法實現

  • 列舉根節點
  • 安全點
  • 安全區域

列舉根節點

之前也說到了,JVM使用的是可達性分析法。但是可達性分析演算法也有他的缺點:

  • 耗費時間
  • GC停頓

耗費時間是因為使用可達性分析演算法的時候尋找GC Root的時候。要掃描整個區域,但是僅僅方法區一般都數百兆。

上文提到可作為GC Root節點的物件有全域性性的引用(常量和靜態屬性),執行上下文(如棧幀中的本地變數表)。

GC停頓是指在進行可達性分析的時候,這項工作必須在一個能確保一致性快照中進行。一致性是指在那一段時間內物件關係不能再反覆發生變化

那麼怎麼縮短這個時間呢?

這個要得益於Hotspot虛擬機器的準確式記憶體管理。因為有他,所以在Hotspot中,是使用一組稱為OopMap的資料結構。在類載入完成的時候,Hotspot就把物件內什麼偏移量上是什麼型別計算出來,在JIT編譯過程中,也會在特定的位置記錄下棧和暫存器中哪些位置是引用。

這樣的話,就不需要進行全盤掃描,只需要掃描OopMap就可以完成GC Root的列舉

安全點

作用

使用OopMap貌似是解決了時間的問題,但是如果為每一條指令都生成OopMap,那麼空間的開銷非常大。因為OopMap內容變化的指令非常多。

所以安全點的作用實際上是解決OopMap空間開銷的問題

概念

那麼什麼地方的節點稱為"安全點"呢?只在特定的位置記錄指令資訊,這些位置叫安全點。

為啥叫安全點呢?因為程式執行時並非在所有地方都能停頓下來開始GC,只有到達特定節點——安全點才能暫停。因為他們已經"安全“了,可以放心的進行分析了

選取標準

問題又來了,那麼以什麼標準來選取呢?選取的不能太少,免得GC等待時間長;也不能太多,免得頻繁進行。

所以選取的標準是:是否具有讓程式長時間執行的特徵

最明顯的特徵就是——指令複用

  • 方法呼叫
  • 迴圈跳轉
  • 異常跳轉

如何完成

那麼怎麼讓執行緒跑到安全點然後讓他們停下來呢?

有兩種實現方式

  • 搶先式中斷
  • 主動式中斷(基本採用這種)

搶先式中斷:不需要執行緒主動配合,發生GC時,所有的執行緒中斷,如果發現有的執行緒沒有跑到安全點,就讓他再執行,跑到安全點再中斷

主動式中斷:當GC需要中斷執行緒的時候,不需要操作執行緒。僅僅設定一個標誌位,然後讓執行緒主動去輪詢他,發現標誌位為真,就中斷。輪詢標誌位和安全點是重合的

安全區域

安全點似乎已經解決了列舉根節點時的空間效率問題,但是還存在一個缺陷:當我的執行緒沒有執行的時候咋辦?比如我的執行緒這個時候正在sleep狀態或者block狀態,那我不可能讓他們自己走到相應的安全點。針對這個情況,Hotspot中提出了一個新的解決方法——安全區域

清楚了上面安全點的概念,那麼你就可以把安全區域當成是他的擴充,這裡一大片的地方都是安全的——引用關係都不會發生變化

當執行緒執行到安全區域中的程式碼時,就先標示自己已經進入到了安全區域,這樣的話,當在這段時間JVM要發起GC的時候,就不用管標示為安全區域的執行緒了。

當執行緒要離開安全區域時,他要判斷是否已經完成了根節點的列舉,如果完成了那就繼續執行,沒有完成的話那就只能等待直到收到可以離開安全區的訊號為止

總結

  1. JVM採用可達性分析方法找出需要回收的物件

  2. 需要回收的物件主要是

    • 廢棄常量
    • 無用的類
  3. java中的四種引用

    • 強引用
    • 軟引用
    • 弱引用
    • 虛引用
  4. Hotspot採用分代回收演算法

    • 新生代採用標記-複製
    • 老年代採用標記-整理
  5. Hotspot列舉根節點的時候演算法實現

    • OopMap
    • 安全點
    • 安全區域

相關文章