垃圾回收機制是 Java 很是重要的特性之一,也是面試題的常客。它讓開發者無需關注空間的建立和釋放,而是以守護進程的形式在後臺自動回收垃圾。這樣作不只提升了開發效率,更改善了內存的使用情況。面試
不少讀者對於垃圾回收機制理解不到位,學了又忘。算法
所以,本文不只僅是對垃圾回收機制進行原理講解,更重要的是,帶着你來一塊兒設計垃圾回收機制,讓你真正搞懂垃圾回收的原理。數組
看文章以前,先拋出幾個問題,你能夠簡單思考下:設計
什麼是Java堆內存?3d
Java 堆是在 JVM 啓動時建立的,主要用來維護運行時數據,如運行過程當中建立的對象和數組都是基於這塊內存空間。Java 堆是很是重要的元素,若是咱們動態建立的對象沒有獲得及時回收,持續堆積,最後會致使堆空間被佔滿,內存溢出。cdn
所以,Java 提供了一種垃圾回收機制,在後臺建立一個守護進程。該進程會在內存緊張的時候自動跳出來,把堆空間的垃圾所有進行回收,從而保證程序的正常運行。對象
那什麼是垃圾呢?blog
所謂「垃圾」,就是指全部再也不存活的對象。常見的判斷是否存活有兩種方法:引用計數法和可達性分析。進程
1. 引用計數法內存
爲每個建立的對象分配一個引用計數器,用來存儲該對象被引用的個數。當該個數爲零,意味着沒有人再使用這個對象,能夠認爲「對象死亡」。可是,這種方案存在嚴重的問題,就是沒法檢測「循環引用」:當兩個對象互相引用,即時它倆都不被外界任何東西引用,它倆的計數都不爲零,所以永遠不會被回收。而實際上對於開發者而言,這兩個對象已經徹底沒有用處了。
所以,Java 裏沒有采用這樣的方案來斷定對象的「存活性」。
2. 可達性分析
這種方案是目前主流語言裏採用的對象存活性判斷方案。基本思路是把全部引用的對象想象成一棵樹,從樹的根結點 GC Roots 出發,持續遍歷找出全部鏈接的樹枝對象,這些對象則被稱爲「可達」對象,或稱「存活」對象。其他的對象則被視爲「死亡」的「不可達」對象,或稱「垃圾」。
參考下圖,object5, object6 和 object7 即是不可達對象,視爲「死亡狀態」,應該被垃圾回收器回收。
3. GC Roots 究竟指誰呢?
咱們能夠猜想,GC Roots 自己必定是可達的,這樣從它們出發遍歷到的對象才能保證必定可達。那麼,Java 裏有哪些對象是必定可達呢?主要有如下四種:
很多讀者可能對這些 GC Roots 似懂非懂,這涉及到 JVM 自己的內存結構等等,將來的文章會再作深刻講解。這裏只要知道有這麼幾種類型的 GC Roots,每次垃圾回收器會從這些根結點開始遍歷尋找全部可達節點。
有幾種回收垃圾的方式呢?
上面已經知道,全部GC Roots不可達的對象都稱爲垃圾,參考下圖,黑色的表示垃圾,灰色表示存活對象,綠色表示空白空間。
那麼,咱們如何來回收這些垃圾呢?
1. 標記-清理
第一步,所謂「標記」就是利用可達性遍歷堆內存,把「存活」對象和「垃圾」對象進行標記,獲得的結果如上圖;第二步,既然「垃圾」已經標記好了,那咱們再遍歷一遍,把全部「垃圾」對象所佔的空間直接清空便可。
結果以下:
這即是標記-清理方案,簡單方便,可是容易產生內存碎片。
2. 標記-整理
既然上面的方法會產生內存碎片,那好,我在清理的時候,把全部存活對象扎堆到同一個地方,讓它們待在一塊兒,這樣就沒有內存碎片了。
結果以下:
這兩種方案適合存活對象多、垃圾少的狀況,它只須要清理掉少許的垃圾,而後挪動下存活對象就能夠了。
3. 複製
這種方法比較粗暴,直接把堆內存分紅兩部分,一段時間內只容許在其中一塊內存上進行分配,當這塊內存被分配完後,則執行垃圾回收,把全部存活對象所有複製到另外一塊內存上,當前內存則直接所有清空。
參考下圖:
起初時只使用上面部分的內存,直到內存使用完畢,才進行垃圾回收,把全部存活對象搬到下半部分,並把上半部分進行清空。
這種作法不容易產生碎片,也簡單粗暴;可是,它意味着你在一段時間內只能使用一部分的內存,超過這部份內存的話就意味着堆內存裏頻繁的複製清空。
這種方案適合存活對象少、垃圾多的狀況,這樣在複製時就不須要複製多少對象過去,多數垃圾直接被清空處理。
Java 的分代回收機制
上面咱們看到有至少三種方法來回收內存,那麼 Java 裏是如何選擇利用這三種回收算法呢?是隻用一種仍是三種都用呢?
1. Java 的堆結構
在選擇回收算法前,咱們先來看一下 Java 堆的結構。
一塊 Java 堆空間通常分紅三部分,這三部分用來存儲三類數據:
也就是說,常規的 Java 堆至少包括了 新生代 和 老年代 兩塊內存區域,並且這兩塊區域有很明顯的特徵:
結合新生代/老年代的存活對象特色和以前提過的幾種垃圾回收算法,能夠獲得以下的回收方案。
2. 新生代-複製回收機制
對於新生代區域,因爲每次 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,依次取名爲 Eden、Survivor A、Survivor B區,其中Eden意爲伊甸園,形容有不少新生對象在裏面建立;Survivor區則爲倖存者,即經歷 GC 後仍然存活下來的對象。
那麼,揭開神祕的面紗。真正的 JVM 工做原理以下,別眨眼:
注意:在真實的 JVM 環境裏,能夠經過參數 SurvivorRatio 手動配置Eden區和單個Survivor區的比例,默認爲8。
那麼,所謂的 Old 區垃圾回收,或稱Major GC,應該如何執行呢?
3. 老年代-標記整理回收機制
根據上面咱們知道,老年代通常存放的是存活時間較久的對象,因此每一次 GC 時,存活對象比較較大,也就是說每次只有少部分對象被回收。
所以,根據不一樣回收機制的特色,這裏選擇存活對象多,垃圾少的標記整理回收機制,僅僅經過少許地移動對象就能清理垃圾,並且不存在內存碎片化。
至此,咱們已經瞭解了 Java 堆內存的分代原理,並瞭解了不一樣代根據各自特色採用了不一樣的回收機制,即新生代採用回收機制,老年代採用標記整理機制。
歡迎關注小編,和我一塊兒,逆風向上。