JVM垃圾回收

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


文章,筆記,源碼地址:github.com/leosanqing/…java


Java 因爲JVM自帶垃圾回收的機制,因此對於不少初中級的程序猿很是方便,不須要本身寫語句控制垃圾的回收,只須要關注業務邏輯便可。尤爲是本身寫demo或者練手的項目,基本不用擔憂垃圾回收的事情git

可是,對於線上的項目,一旦出現垃圾回收的問題,每每是很是致命的。因此不管是校招仍是社招基本都會問到JVM的垃圾回收機制以及算法github

這個文章的結構以下算法

  • 哪些內存須要回收
  • 何時回收
  • 如何回收

哪些內存須要回收

主要分爲兩個部分安全

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

若是你知道 JVM運行時數據區,那麼應該知道上面的兩部分恰好是按照線程共不共享來劃分的。這樣也方便記數據結構

若是不瞭解的話,能夠參考個人這篇 JVM運行時數據區的文章spa

第一部分

剛剛也提到了,若是瞭解JVM運行時數據區的話,第一部分的線程是不共享的他們都隨着線程生而生,線程滅而滅線程

這三個區域的內存分配和回收都具有肯定性,沒必要過多考慮回收問題,在方法結束或者線程結束時,內存天然而然就跟着回收了3d

第二部分

基本是運行時才已知,且線程共享,基本上考慮垃圾回收算法以及回收斷定都是這兩個部分。

方法區的回收

在方法區的回收垃圾的性價比通常是比較低的

在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將新生代的 Eden區和其中一個survivor區域劃分比例調整爲8:1.而survivor共有兩個,每次只用其中一個,另外一個用來複制,存放活着的對象

標記-整理

過程

標記過程同樣,就是清除的時候,將全部活着的對象移到一端。而後清理剩下的區域

缺點

  • 回收效率不高。

因此通常用在老年代的回收

分代收集算法

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

主要是根據不一樣代的特色,選擇相對合適的上述算法。

好比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
    • 安全點
    • 安全區域
相關文章
相關標籤/搜索