當前位置:
首頁 > 知識 > Java垃圾回收機制詳解

Java垃圾回收機制詳解


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

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

  • 什麼是堆內存?

  • 什麼是垃圾?

  • 有哪些方法回收這些垃圾?

  • 什麼是分代回收機制?

什麼是 Java 堆內存

堆是在 JVM 啟動時創建的,主要用來維護運行時數據,如運行過程中創建的對象和數組都是基於這塊內存空間。Java 堆是非常重要的元素,如果我們動態創建的對象沒有得到及時回收,持續堆積,最後會導致堆空間被佔滿,內存溢出。

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


那什麼是垃圾呢?

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

引用計數法

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

因此,Java 里沒有採用這樣的方案來判定對象的「存活性」。

可達性分析

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

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

Java垃圾回收機制詳解

GC Roots 究竟指誰呢?

我們可以猜測,GC Roots 本身一定是可達的,這樣從它們出發遍歷到的對象才能保證一定可達。那麼,Java 里有哪些對象是一定可達呢?主要有以下四種:

  • 虛擬機棧(幀棧中的本地變數表)中引用的對象。

  • 方法區中靜態屬性引用的對象。

  • 方法區中常量引用的對象。

  • 本地方法棧中JNI引用的對象。

不少讀者可能對這些 GC Roots 似懂非懂,這涉及到 JVM 本身的內存結構等等,未來的文章會再做深入講解。這裡只要知道有這麼幾種類型的 GC Roots,每次垃圾回收器會從這些根結點開始遍歷尋找所有可達節點。


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

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

Java垃圾回收機制詳解

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

標記-清理

第一步,所謂「標記」就是利用可達性遍歷堆內存,把「存活」對象和「垃圾」對象進行標記,得到的結果如上圖;

第二步,既然「垃圾」已經標記好了,那我們再遍歷一遍,把所有「垃圾」對象所佔的空間直接清空即可。

結果如下:

Java垃圾回收機制詳解

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

標記-整理

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

結果如下:

Java垃圾回收機制詳解

這兩種方案適合存活對象多,垃圾少的情況,它只需要清理掉少量的垃圾,然後挪動下存活對象就可以了。

複製

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

參考下圖:

Java垃圾回收機制詳解

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

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

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


Java 的分代回收機制

上面我們看到有至少三種方法來回收內存,那麼 Java 里是如何選擇利用這三種回收演算法呢?是只用一種還是三種都用呢?

Java 的堆結構

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

一塊 Java 堆空間一般分成三部分,這三部分用來存儲三類數據:

  • 剛剛創建的對象。在代碼運行時會持續不斷地創造新的對象,這些新創建的對象會被統一放在一起。因為有很多局部變數等在新創建後很快會變成不可達的對象,快速死去,因此這塊區域的特點是存活對象少,垃圾多。形象點描述這塊區域為:新生代;

  • 存活了一段時間的對象。這些對象早早就被創建了,而且一直活了下來。我們把這些存活時間較長的對象放在一起,它們的特點是存活對象多,垃圾少。形象點描述這塊區域為:老年代;

  • 永久存在的對象。比如一些靜態文件,這些對象的特點是不需要垃圾回收,永遠存活。形象點描述這塊區域為:永久代。(不過在 Java 8 里已經把永久代刪除了,把這塊內存空間給了元空間,後續文章再講解。)

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

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

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

結合新生代/老年代的存活對象特點和之前提過的幾種垃圾回收演算法,可以得到如下的回收方案:

新生代-複製回收機制

對於新生代區域,由於每次 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 後仍然存活下來的對象。

工作原理如下:

  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 工程師的必經之路。

如有問題歡迎與我聯繫。


學習Java的同學注意了!!!

學習過程中遇到什麼問題或者想獲取學習資源的話,歡迎加入Java學習交流群495273252,我們一起學Java!

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Java團長 的精彩文章:

Java——IO 流
Java開發中如何正確踩坑?
《實戰java高並發程序設計》源碼整理及讀書筆記
JavaSE的自動裝箱和自動拆箱
程序員如何選擇未來的職業路線

TAG:Java團長 |

您可能感興趣

Redis 內存淘汰機制詳解
async/await使用深入詳解
詳解ADI收購Linear
實用!加拿大Super Visa申辦條件及資料詳解
stringr包詳解
HashMap詳解
eBay運營之best match規則詳解
Summary 數據類型詳解
.gitignore詳解及編寫
Linux wget 命令用法詳解
MyBatis配置文件詳解
想登錄Office辦公軟體的好夥伴Outlook?配置步驟詳解來了!
Air Jordan 11低幫改裝,鞋底solo氣墊改zoom詳解圖
詳解TogetherJS
Windows窗體數據抓取詳解
Windows系統下如何搭建Node.js伺服器詳解
詳解Android UI線程卡頓收集
OpenStack之Magnum容器編服務排引擎詳解
詳解 RestTemplate 操作
MyBatis 配置 typeHandlers 詳解