真正學懂 Java 垃圾回收機制

垃圾回收機制是 Java 很是重要的特性之一,也是面試題的常客。它讓開發者無需關注空間的建立和釋放,而是以守護進程的形式在後臺自動回收垃圾。這樣作不只提升了開發效率,更改善了內存的使用情況。面試

不少讀者對於垃圾回收機制理解不到位,學了又忘算法

所以,本文不只僅是對垃圾回收機制進行原理講解,更重要的是,帶着你來一塊兒設計垃圾回收機制,讓你真正搞懂垃圾回收的原理數組

看文章以前,先拋出幾個問題,你能夠簡單思考下:設計

  • 什麼是堆內存?
  • 什麼是垃圾?
  • 有哪些方法回收這些垃圾?
  • 爲何要分代回收機制?


什麼是Java堆內存?3d

Java 堆是在 JVM 啓動時建立的,主要用來維護運行時數據,如運行過程當中建立的對象和數組都是基於這塊內存空間。Java 堆是很是重要的元素,若是咱們動態建立的對象沒有獲得及時回收,持續堆積,最後會致使堆空間被佔滿,內存溢出。cdn

所以,Java 提供了一種垃圾回收機制,在後臺建立一個守護進程。該進程會在內存緊張的時候自動跳出來,把堆空間的垃圾所有進行回收,從而保證程序的正常運行。對象

那什麼是垃圾呢?blog

所謂「垃圾」,就是指全部再也不存活的對象。常見的判斷是否存活有兩種方法:引用計數法可達性分析進程

1. 引用計數法內存

爲每個建立的對象分配一個引用計數器,用來存儲該對象被引用的個數。當該個數爲零,意味着沒有人再使用這個對象,能夠認爲「對象死亡」。可是,這種方案存在嚴重的問題,就是沒法檢測「循環引用」:當兩個對象互相引用,即時它倆都不被外界任何東西引用,它倆的計數都不爲零,所以永遠不會被回收。而實際上對於開發者而言,這兩個對象已經徹底沒有用處了。

所以,Java 裏沒有采用這樣的方案來斷定對象的「存活性」。

2. 可達性分析

這種方案是目前主流語言裏採用的對象存活性判斷方案。基本思路是把全部引用的對象想象成一棵樹,從樹的根結點 GC Roots 出發,持續遍歷找出全部鏈接的樹枝對象,這些對象則被稱爲「可達」對象,或稱「存活」對象。其他的對象則被視爲「死亡」的「不可達」對象,或稱「垃圾」。

參考下圖,object5, object6 和 object7 即是不可達對象,視爲「死亡狀態」,應該被垃圾回收器回收。



此次,真正學懂 Java 垃圾回收機制



3. GC Roots 究竟指誰呢?

咱們能夠猜想,GC Roots 自己必定是可達的,這樣從它們出發遍歷到的對象才能保證必定可達。那麼,Java 裏有哪些對象是必定可達呢?主要有如下四種:

  • 虛擬機棧(幀棧中的本地變量表)中引用的對象。
  • 方法區中靜態屬性引用的對象。
  • 方法區中常量引用的對象。
  • 本地方法棧中JNI引用的對象。

很多讀者可能對這些 GC Roots 似懂非懂,這涉及到 JVM 自己的內存結構等等,將來的文章會再作深刻講解。這裏只要知道有這麼幾種類型的 GC Roots,每次垃圾回收器會從這些根結點開始遍歷尋找全部可達節點。


有幾種回收垃圾的方式呢?

上面已經知道,全部GC Roots不可達的對象都稱爲垃圾,參考下圖,黑色的表示垃圾,灰色表示存活對象,綠色表示空白空間。


此次,真正學懂 Java 垃圾回收機制


那麼,咱們如何來回收這些垃圾呢?

1. 標記-清理

第一步,所謂「標記」就是利用可達性遍歷堆內存,把「存活」對象和「垃圾」對象進行標記,獲得的結果如上圖;第二步,既然「垃圾」已經標記好了,那咱們再遍歷一遍,把全部「垃圾」對象所佔的空間直接清空便可。

結果以下:


此次,真正學懂 Java 垃圾回收機制


這即是標記-清理方案,簡單方便,可是容易產生內存碎片

2. 標記-整理

既然上面的方法會產生內存碎片,那好,我在清理的時候,把全部存活對象扎堆到同一個地方,讓它們待在一塊兒,這樣就沒有內存碎片了。

結果以下:

此次,真正學懂 Java 垃圾回收機制

這兩種方案適合存活對象多、垃圾少的狀況,它只須要清理掉少許的垃圾,而後挪動下存活對象就能夠了。

3. 複製

這種方法比較粗暴,直接把堆內存分紅兩部分,一段時間內只容許在其中一塊內存上進行分配,當這塊內存被分配完後,則執行垃圾回收,把全部存活對象所有複製到另外一塊內存上,當前內存則直接所有清空。

參考下圖:


此次,真正學懂 Java 垃圾回收機制


起初時只使用上面部分的內存,直到內存使用完畢,才進行垃圾回收,把全部存活對象搬到下半部分,並把上半部分進行清空。

這種作法不容易產生碎片,也簡單粗暴;可是,它意味着你在一段時間內只能使用一部分的內存,超過這部份內存的話就意味着堆內存裏頻繁的複製清空。

這種方案適合存活對象少、垃圾多的狀況,這樣在複製時就不須要複製多少對象過去,多數垃圾直接被清空處理。


Java 的分代回收機制

上面咱們看到有至少三種方法來回收內存,那麼 Java 裏是如何選擇利用這三種回收算法呢?是隻用一種仍是三種都用呢?

1. Java 的堆結構

在選擇回收算法前,咱們先來看一下 Java 堆的結構。

一塊 Java 堆空間通常分紅三部分,這三部分用來存儲三類數據:

  • 剛剛建立的對象。在代碼運行時會持續不斷地創造新的對象,這些新建立的對象會被統一放在一塊兒。由於有不少局部變量等在新建立後很快會變成不可達的對象,快速死去,所以這塊區域的特色是存活對象少,垃圾多。形象點描述這塊區域爲:新生代
  • 存活了一段時間的對象。這些對象早早就被建立了,並且一直活了下來。咱們把這些存活時間較長的對象放在一塊兒,它們的特色是存活對象多,垃圾少。形象點描述這塊區域爲:老年代
  • 永久存在的對象。好比一些靜態文件,這些對象的特色是不須要垃圾回收,永遠存活。形象點描述這塊區域爲:永久代。(不過在 Java 8 裏已經把永久代刪除了,把這塊內存空間給了元空間,關注公衆號,後續文章再講解。)


也就是說,常規的 Java 堆至少包括了 新生代 和 老年代 兩塊內存區域,並且這兩塊區域有很明顯的特徵:

  • 新生代:存活對象少、垃圾多
  • 老年代:存活對象多、垃圾少

結合新生代/老年代的存活對象特色和以前提過的幾種垃圾回收算法,能夠獲得以下的回收方案。

2. 新生代-複製回收機制

對於新生代區域,因爲每次 GC 都會有大量新對象死去,只有少許存活。所以採用複製回收算法,GC 時把少許的存活對象複製過去便可。

那麼如何設計這個複製算法比較好呢?有如下幾種方式:

思路1: 把內存均分紅 1:1 兩等份

以下圖拆份內存。

此次,真正學懂 Java 垃圾回收機制

每次只使用一半的內存,當這一半滿了後,就進行垃圾回收,把存活的對象直接複製到另外一半內存,並清空當前一半的內存。

這種分法的缺陷是至關於只有一半的可用內存,對於新生代而言,新對象持續不斷地被建立,若是隻有一半可用內存,那顯然要持續不斷地進行垃圾回收工做,反而影響到了正常程序的運行,得不償失。

思路2: 把內存按 9:1 分

既然上面的分法致使可用內存只剩一半,那麼我作些調整,把 1:1 變成9:1,


此次,真正學懂 Java 垃圾回收機制

最開始在 9 的內存區使用,當 9 快要滿時,執行復制回收,把 9 內仍然存活的對象複製到 1 區,並清空 9 區。

這樣看起來是比上面的方法好了,可是它存在比較嚴重的問題。

當咱們把 9 區存活對象複製到 1 區時,因爲內存空間比例相差比較大,因此頗有可能 1 區放不滿,此時就不得不把對象移到 老年區。而這就意味着,可能會有一部分 並不老 的 9 區對象因爲 1 區放不下了而被放到了 老年區,可想而知,這破壞了 老年區 的規則。或者說,必定程度上的 老年區 並不必定全是 老年對象

那應該如何才能把真正比較老的對象挪到 老年區 呢?

思路3: 把內存按 8:1:1 分


此次,真正學懂 Java 垃圾回收機制


既然 9:1 有可能把年輕對象放到 老年區,那就換成 8:1:1,依次取名爲 Eden、Survivor A、Survivor B區,其中Eden意爲伊甸園,形容有不少新生對象在裏面建立;Survivor區則爲倖存者,即經歷 GC 後仍然存活下來的對象。

那麼,揭開神祕的面紗。真正的 JVM 工做原理以下,別眨眼:

  1. 首先,Eden 區最大,對外提供堆內存。當 Eden 區快要滿了,則進行 Minor GC,把存活對象放入 Survivor A 區,清空 Eden 區;
  2. Eden 區被清空後,繼續對外提供堆內存;
  3. 當Eden 區再次被填滿,此時對 Eden 區和 Survivor A區同時進行 Minor GC,把存活對象放入 Survivor B 區,同時清空 Eden 區和 Survivor A 區;
  4. Eden 區繼續對外提供堆內存,並重覆上述過程,即在 Eden 區填滿後,把Eden 區和某個 Survivor 區的存活對象放到另外一個 Survivor 區;
  5. 當某個 Survivor 區被填滿,且仍有對象未被複制完畢時,或者某些對象在反覆 Survive 15 次左右時,則把這部分剩餘對象放到 Old 區;
  6. 當 Old 區也被填滿時,進行 Major GC,對 Old 區進行垃圾回收。


注意:在真實的 JVM 環境裏,能夠經過參數 SurvivorRatio 手動配置Eden區和單個Survivor區的比例,默認爲8。

那麼,所謂的 Old 區垃圾回收,或稱Major GC,應該如何執行呢?

3. 老年代-標記整理回收機制

根據上面咱們知道,老年代通常存放的是存活時間較久的對象,因此每一次 GC 時,存活對象比較較大,也就是說每次只有少部分對象被回收。

所以,根據不一樣回收機制的特色,這裏選擇存活對象多,垃圾少的標記整理回收機制,僅僅經過少許地移動對象就能清理垃圾,並且不存在內存碎片化。

至此,咱們已經瞭解了 Java 堆內存的分代原理,並瞭解了不一樣代根據各自特色採用了不一樣的回收機制,即新生代採用回收機制,老年代採用標記整理機制。

歡迎關注小編,和我一塊兒,逆風向上。

相關文章
相關標籤/搜索