Java 技術之垃圾回收機制

垃圾回收機制是 Java 非常重要的特性之一,也是面試題的常客。它讓開發者無需關注空間的創建和釋放,而是以守護進程的形式在后臺自動回收垃圾。這樣做不僅提高了開發效率,更改善了內存的使用狀況。
今天本文來對垃圾回收機制進行講解,主要涉及下面幾個問題:

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

什么是 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

http://wingjay.com

參考文章:

Android技術·面試技巧·職業感悟
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容