《億級Android架構》小專欄文章列表:git
《Android 架構之網絡鏈接與加速》github
《Android 架構之網絡安全演進》segmentfault
垃圾回收機制是 Java 很是重要的特性之一,也是面試題的常客。它讓開發者無需關注空間的建立和釋放,而是以守護進程的形式在後臺自動回收垃圾。這樣作不只提升了開發效率,更改善了內存的使用情況。微信
今天本文來對垃圾回收機制進行講解,主要涉及下面幾個問題:markdown
堆是在 JVM 啓動時建立的,主要用來維護運行時數據,如運行過程當中建立的對象和數組都是基於這塊內存空間。Java 堆是很是重要的元素,若是咱們動態建立的對象沒有獲得及時回收,持續堆積,最後會致使堆空間被佔滿,內存溢出。
所以,Java 提供了一種垃圾回收機制,在後臺建立一個守護進程。該進程會在內存緊張的時候自動跳出來,把堆空間的垃圾所有進行回收,從而保證程序的正常運行。
所謂「垃圾」,就是指全部再也不存活的對象。常見的判斷是否存活有兩種方法:引用計數法和可達性分析。
爲每個建立的對象分配一個引用計數器,用來存儲該對象被引用的個數。當該個數爲零,意味着沒有人再使用這個對象,能夠認爲「對象死亡」。可是,這種方案存在嚴重的問題,就是沒法檢測「循環引用」:當兩個對象互相引用,即時它倆都不被外界任何東西引用,它倆的計數都不爲零,所以永遠不會被回收。而實際上對於開發者而言,這兩個對象已經徹底沒有用處了。
所以,Java 裏沒有采用這樣的方案來斷定對象的「存活性」。
這種方案是目前主流語言裏採用的對象存活性判斷方案。基本思路是把全部引用的對象想象成一棵樹,從樹的根結點 GC Roots 出發,持續遍歷找出全部鏈接的樹枝對象,這些對象則被稱爲「可達」對象,或稱「存活」對象。其他的對象則被視爲「死亡」的「不可達」對象,或稱「垃圾」。
參考下圖,object5,object6和object7即是不可達對象,視爲「死亡狀態」,應該被垃圾回收器回收。
咱們能夠猜想,GC Roots 自己必定是可達的,這樣從它們出發遍歷到的對象才能保證必定可達。那麼,Java 裏有哪些對象是必定可達呢?主要有如下四種:
很多讀者可能對這些 GC Roots 似懂非懂,這涉及到 JVM 自己的內存結構等等,將來的文章會再作深刻講解。這裏只要知道有這麼幾種類型的 GC Roots,每次垃圾回收器會從這些根結點開始遍歷尋找全部可達節點。
上面已經知道,全部GC Roots不可達的對象都稱爲垃圾,參考下圖,黑色的表示垃圾,灰色表示存活對象,綠色表示空白空間。
那麼,咱們如何來回收這些垃圾呢?
第一步,所謂「標記」就是利用可達性遍歷堆內存,把「存活」對象和「垃圾」對象進行標記,獲得的結果如上圖; 第二步,既然「垃圾」已經標記好了,那咱們再遍歷一遍,把全部「垃圾」對象所佔的空間直接清空
便可。
結果以下:
這即是標記-清理
方案,簡單方便
,可是容易產生內存碎片
。
既然上面的方法會產生內存碎片,那好,我在清理的時候,把全部存活
對象扎堆到同一個地方,讓它們待在一塊兒,這樣就沒有內存碎片了。
結果以下:
這兩種方案適合存活對象多,垃圾少
的狀況,它只須要清理掉少許的垃圾,而後挪動下存活對象就能夠了。
這種方法比較粗暴,直接把堆內存分紅兩部分,一段時間內只容許在其中一塊內存上進行分配,當這塊內存被分配完後,則執行垃圾回收,把全部存活
對象所有複製到另外一塊內存上,當前內存則直接所有清空。
參考下圖:
起初時只使用上面部分的內存,直到內存使用完畢,才進行垃圾回收,把全部存活對象搬到下半部分,並把上半部分進行清空。這種作法不容易產生碎片,也簡單粗暴;可是,它意味着你在一段時間內只能使用一部分的內存,超過這部份內存的話就意味着堆內存裏頻繁的複製清空
。
這種方案適合存活對象少,垃圾多
的狀況,這樣在複製時就不須要複製多少對象過去,多數垃圾直接被清空處理。
上面咱們看到有至少三種方法來回收內存,那麼 Java 裏是如何選擇利用這三種回收算法呢?是隻用一種仍是三種都用呢?
在選擇回收算法前,咱們先來看一下 Java 堆的結構。
一塊 Java 堆空間通常分紅三部分,這三部分用來存儲三類數據:
不可達
的對象,快速死去
,所以這塊區域的特色是存活對象少,垃圾多
。形象點描述這塊區域爲:新生代
;存活時間較長
的對象放在一塊兒,它們的特色是存活對象多,垃圾少
。形象點描述這塊區域爲:老年代
;永久代
。(不過在 Java 8 裏已經把永久代
刪除了,把這塊內存空間給了元空間
,後續文章再講解。)也就是說,常規的 Java 堆至少包括了 新生代
和 老年代
兩塊內存區域,並且這兩塊區域有很明顯的特徵:
結合新生代/老年代的存活對象特色和以前提過的幾種垃圾回收算法,能夠獲得以下的回收方案:
複製
回收機制對於新生代區域,因爲每次 GC 都會有大量新對象死去,只有少許存活。所以採用複製
回收算法,GC 時把少許的存活對象複製過去便可。
那麼如何設計這個複製
算法比較好呢?有如下幾種方式:
1:1
兩等份以下圖拆份內存。
每次只使用一半的內存,當這一半滿了後,就進行垃圾回收,把存活的對象直接複製到另外一半內存,並清空當前一半的內存。這種分法的缺陷是至關於只有一半的可用內存,對於新生代而言,新對象持續不斷地被建立,若是隻有一半可用內存,那顯然要持續不斷地進行垃圾回收工做,反而影響到了正常程序的運行,得不償失。
9:1
分既然上面的分法致使可用內存只剩一半,那麼我作些調整,把 1:1
變成9:1
,
9
的內存區使用,當
9
快要滿時,執行復制回收,把
9
內仍然存活的對象複製到
1
區,並清空
9
區。
這樣看起來是比上面的方法好了,可是它存在比較嚴重的問題。
當咱們把 9
區存活對象複製到 1
區時,因爲內存空間比例相差比較大,因此頗有可能 1
區放不滿,此時就不得不把對象移到 老年區
。而這就意味着,可能會有一部分 並不老
的 9
區對象因爲 1
區放不下了而被放到了 老年區
,可想而知,這破壞了 老年區
的規則。或者說,必定程度上的 老年區
並不必定全是 老年對象
。
那應該如何才能把真正比較 老
的對象挪到 老年區
呢?
8:1:1
分9:1
有可能把年輕對象放到
老年區
,那就換成
8:1:1
,依次取名爲
Eden
、
Survivor A
、
Survivor B
區,其中
Eden
意爲伊甸園,形容有不少新生對象在裏面建立;
Survivor
區則爲倖存者,即經歷 GC 後仍然存活下來的對象。
工做原理以下:
Eden
區最大,對外提供堆內存。當 Eden
區快要滿了,則進行 Minor GC
,把存活對象放入Survivor A
區,清空 Eden
區;Eden
區被清空後,繼續對外提供堆內存;Eden
區再次被填滿,此時對Eden
區和Survivor A
區同時進行 Minor GC
,把存活對象放入Survivor B
區,同時清空Eden
區和Survivor A
區;Eden
區繼續對外提供堆內存,並重覆上述過程,即在Eden
區填滿後,把Eden
區和某個Survivor
區的存活對象放到另外一個Survivor
區;Survivor
區被填滿,且仍有對象未被複制完畢時,或者某些對象在反覆Survive
15
次左右時,則把這部分剩餘對象放到Old
區;Old
區也被填滿時,進行 Major GC
,對 Old
區進行垃圾回收。[注意,在真實的 JVM 環境裏,能夠經過參數 SurvivorRatio
手動配置Eden
區和單個Survivor
區的比例,默認爲8。]
那麼,所謂的 Old
區垃圾回收,或稱Major GC
,應該如何執行呢?
標記整理
回收機制根據上面咱們知道,老年代通常存放的是存活時間較久的對象,因此每一次 GC 時,存活對象比較較大,也就是說每次只有少部分對象被回收。
所以,根據不一樣回收機制的特色,這裏選擇存活對象多,垃圾少
的標記整理
回收機制,僅僅經過少許地移動對象就能清理垃圾,並且不存在內存碎片化。
至此,咱們已經瞭解了 Java 堆內存的分代原理,並瞭解了不一樣代根據各自特色採用了不一樣的回收機制,即新生代
採用複製回收
機制,老年代
採用標記整理
機制。
垃圾回收是 Java 很是重要的特性,也是高級 Java 工程師的必經之路。
若有問題歡迎與我聯繫。
謝謝。
wingjay
業務的快速增加離不開穩定可靠的架構。《億級Android架構》小專欄會基於做者實際工做經驗,結合國內大廠如阿里、騰訊、美團等基礎架構現狀,嘗試談談如何設計一套好的架構來支持業務從0到1,甚至到億,但願與你們多多探討。
本專欄主要內容:
《億級Android架構》小專欄文章列表:
參考文章:
PS:本文原創發佈於微信公衆號「wingjay」,回覆關鍵字「程序員」獲取一份 15 本程序員經典電子書。