一文學會 Java 垃圾回收機制

《億級Android架構》小專欄文章列表:git

《億級 Android 架構》專欄隨談》程序員

《Android 架構之網絡鏈接與加速》github

《Android 架構之長鏈接技術》面試

《Android 架構之高可用移動網絡鏈接》算法

《Android 架構之網絡安全演進》segmentfault

《Android 架構之高性能移動端日誌系統》數組

《Android 架構之秒級移動配置中心》安全

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

今天本文來對垃圾回收機制進行講解,主要涉及下面幾個問題:markdown

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

什麼是 Java 堆內存

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

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

那什麼是垃圾呢?

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

引用計數法

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

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

可達性分析

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

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

GC Roots 究竟指誰呢?

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

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

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

有哪些方式來回收這些垃圾呢?

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

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

標記-清理

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

結果以下:

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

標記-整理

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

結果以下:

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

複製

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

參考下圖:

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

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

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

Java 的分代回收機制

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

Java 的堆結構

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

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

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

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

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

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

新生代-複製回收機制

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

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

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

以下圖拆份內存。

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

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

思路2. 把內存按 9:1

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

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

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

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

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

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

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

工做原理以下:

  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,應該如何執行呢?

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

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

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

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

小結

垃圾回收是 Java 很是重要的特性,也是高級 Java 工程師的必經之路。

若有問題歡迎與我聯繫。

謝謝。

wingjay


《億級Android架構》小專欄介紹

業務的快速增加離不開穩定可靠的架構。《億級Android架構》小專欄會基於做者實際工做經驗,結合國內大廠如阿里、騰訊、美團等基礎架構現狀,嘗試談談如何設計一套好的架構來支持業務從0到1,甚至到億,但願與你們多多探討。

本專欄主要內容:

  1. 當前大廠有哪些Android架構;
  2. 這些架構能解決什麼問題;
  3. 這些架構的原理是什麼;
  4. 學習這些架構對咱們自身的意義。

《億級Android架構》小專欄文章列表:

《億級 Android 架構》專欄隨談》

《Android 架構之網絡鏈接與加速》

《Android 架構之長鏈接技術》

《Android 架構之高可用移動網絡鏈接》

《Android 架構之網絡安全演進》

《Android 架構之高性能移動端日誌系統》


參考文章:

PS:本文原創發佈於微信公衆號「wingjay」,回覆關鍵字「程序員」獲取一份 15 本程序員經典電子書。

Android架構、技術感悟、我的成長
相關文章
相關標籤/搜索