面試官:JVM 中的堆、堆中的內存分配以及堆中的垃圾回收瞭解嗎?我:要不我直接回去等通知?

我們從前面的文章中知道了,我們創建的對象實例會存放在堆中,也就是對象實例會在堆裏分配內存,所以堆也是垃圾收集的主要區域。但僅僅知道這些是不夠的,今天我們就來具體看一看堆以及堆中的垃圾回收。

這裏在網上找到了一個比較好的堆相關內容的圖,我們就根據這個圖來一步步瞭解堆:

圖片來源於網絡
圖片來源於網絡

我們可以看到,堆主要分成了兩部分:

  1. 新生代(Young Generation)
  2. 老年代(Old Generation)

新生代(Young Generation)又可以分成

  1. Eden 區
  2. Survivor 0 區
  3. Survivor 1 區

其中,Eden 區、Survivor 0 區 也被稱爲 From 區和 To 區。(至於爲什麼要這樣分?不要急,後面會說,這裏先有個分區概念就行。)

我們來先看整體再看細節

整體上看新生代與老年代

整體上可以看到,堆主要分成了兩部分:

  1. 新生代(Young Generation)
  2. 老年代(Old Generation)

對象實例會在新生代裏誕生,當新生代區域內存不夠時,會進行 垃圾回收 ,被稱爲 Minor GC,會將新生代區域裏一些已經不再使用的對象實例給回收掉,釋放出內存。因爲新生代的對象實例大部分存活時間都很短,因此 Minor GC 會頻繁執行,且執行的速度一般也會比較快;而那些還需要繼續使用的對象實例,也就是經過 Minor GC 之後還在的對象實例,年齡會加 1

而當新生代區域裏的某個對象實例年齡到達一定程度時,比如 15(默認是 15 ),這個對象實例會被移動到老年代區域。

或者可以形象的理解爲,一個新兵在戰場(新生代)上,扛過了 15 次 戰爭(Minor GC)之後還存活着,那麼就可以功成名就的去後方指揮部(老年代)當將軍了。

不過,老年代區域的內存也是有限的,當老年代區域的內存不夠時,也會進行 垃圾回收,被稱爲 Major GC 或者 Full GC;並且,進行 Full GC 時,經常會伴隨着至少一次的 Minor GC(但並非絕對會出現)。因爲老年代對象其存活時間長,因此 Full GC 很少執行,且執行速度會比 Minor GC 慢很多。

你可能會有疑問,當老年代發生 Full GC 之後,老年代區域的內存依舊不夠,會怎麼樣?會報 java.lang.OutOfMemoryError: Java heap space 異常,簡稱 OOM ,也就是內存溢出。至於解決的辦法,後面的內容裏會說,這裏先知道一下就行。

做個堆整體上內容的小總結: 堆整體上分成了新生代和老年代, 對象實例在新生代裏誕生;新生代內存不夠時,會進行垃圾回收;當一個對象實例經過一定次數的垃圾回收之後還沒有被回收,就會進入老年代;當老年代內存不夠時,也會進行垃圾回收,而如果老年進行垃圾回收之後,內存依舊不夠,就會出現 OOM 異常。

  • 整體上我們已經瞭解了,但細節上還有一些問題:
  1. 爲什麼新生代要劃分成 Eden 區、From 區、To 區?
  2. 垃圾回收是怎麼進行的?
  3. 怎麼知道一個對象實例是否該回收?
  4. … …

帶着這些疑問,我們再從細節上看一下堆的新生代

細節上看新生代

爲什麼新生代要劃分成 Eden 區、From 區、To 區?垃圾回收的過程?

新生代爲什麼這樣劃分,其實我們來了解一下新生代 Minor GC 的過程就知道了(其實也是順便了解一下複製算法)。

Minor GC 過程:複製→清空→互換

  1. 複製
    首先,當 Eden 區內存滿的時候會觸發第一次 Minor GC,然後把 GC 之後還活着的對象實例拷貝到 From 區;
    當 Eden 區再次觸發 GC 的時候,會掃描 Eden 區和 From 區,對這兩個區域進行 GC;經過這次 GC 回收後還存活的對象,則會直接複製到To區域,並且把這些對象的年齡 +1(如果有對象的年齡已經達到了老年的標準,則會複製到老年代區)。

  2. 清空
    第二次及之後的 Minor GC 是把 GC 之後 Eden 區和 From 區還存活的對象複製到 To 區;複製之後,會把 Eden 區和 From 區都清空掉。

  3. 互換
    然後會將此時的清空後的 From 區會與 To 區交換位置,也就是 From 區變成 To 區,To 變成 From 區;這樣做是爲了保證每一次 To 區都是空的,當下一次 GC 時,就又可以把 From 區的對象實例複製到 To 區了。

所以,你應該爲什麼要將新生代劃分成 Eden 區、From 區、To 區了吧?
Eden 區裏是爲新產生的對象實例準備的,而 From 區、To 區是爲了每次的複製與交換準備的。
(這裏提一句,每次 GC 之後會 From 區與 To 區都會交換,那麼這兩個區的內存大小應該滿足什麼樣的關係?聰明的你應該想到了,爲了滿足每次的交換動作, From 區與 To 區的內存應該是要一樣大的! )

怎麼知道一個對象實例是否可以回收 / 怎麼判斷對象實例是否死亡?

1. 引用計數算法

我們可以給對象實列添加一個引用計數器,每當有一個地方引用這個對象實列時,計數器加 1 ,當這個地方不再引用它時,也就是引用時效時,計數器減 1 ;引用計數器爲 0 的對象實列就是可以被回收的對象,也就是死亡的對象。

但引用計數器算法可能會出現一個問題:循環引用的情況下,使用引用計數器算法進行垃圾回收會出問題。
循環引用指的是 A 對象引用了 B 對象, B 對象引用了 A 對象;如此一來,引用計數器永遠都不會爲 0 ,就會導致無法對它們進行回收。

也正因爲循環引用導致的這個問題,Java 虛擬機沒有使用引用計數器算法來進行垃圾回收。

2. 可達性分析算法

以 GC Roots 爲起始點進行搜索,可達的對象都是存活的,不可達的對象可被回收。

在 Java 技術體系裏面,固定可以作爲 GC Roots 的的對象包含如下幾種:

  1. 在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用調用的方法堆棧中使用的參數、局部變量、臨時變量等。
  2. 在方法區中類靜態變量屬性引用的對象,比如 Java 類的引用類型靜態變量。
  3. 在方法區中常量引用的對象,比如字符串常量池裏的引用。
  4. 在本地方法棧中JNI(也就是 Native 方法)引用的對象。
  5. Java 虛擬機內部的引用,如基本數據類型對應的 Class 對象,一些常駐的異常對象(比如空指針異常,OOM)等,還有系統加載器。
  6. 所有被同步鎖(synchronized 關鍵字)持有的對象。
  7. 反映 Java 虛擬機內部清空的 JMXBean、JVMTI 中註冊的回調、本地代碼緩存等。

除了上面這些固定的 GC Roots ,還可以根據用戶所選用的垃圾收集器以及當前回收的內存區域不同,加入其他“臨時性”的對象。

垃圾怎麼回收的?(垃圾回收算法有哪些?)
  1. 複製算法
    這個算法,我們在講新生代的垃圾回收時,說的就是這個算法。
    簡單來說就是:將內存分爲大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完後,將剩下還存活的對象複製到另一塊去,然後再把使用的空間進行一次清理。
    好處是:效率高,不會產生大量內存碎片(內存碎片:對象在內存中不連續,這一個那一個,是內存碎片化了)。
    壞處是:只使用了一半的內存,浪費了內存。

  2. 標記-清除算法
    這個算法其實就和它的名字一樣 標記-清除:首先將需要回收的對象標記一下,當內存不夠觸發垃圾回收時,就會將所有標記了的對象清除掉。
    這個算法很簡單,但是問題也很明顯,因爲標記對象的位置不確定,所以會產生大量內存碎片,並且標記清除過程的效率不高。

  3. 標記-整理算法
    標記-整理算法,也同樣會對需要回收的對象進行標記,但後續不是直接清除標記過的對象,而是讓所有存活對象(也就是未標記對象)全部向內存的一端移動,然後再清理掉端邊界以外的內存。
    相比與標記-清除算法,標記-整理算法不會產生內存碎片;但是同樣,效率不高,因爲需要移動大量對象,所以處理效率自然不高。

  4. 分代收集算法
    分代收集算法其實不是什麼新算法,而是將上面說的三種揚長避短,根據不同的情況使用不同的算法。
    分代收集算法會根據對象存活週期將內存劃分爲不同的幾個部分,一般就是我們前面說的分成 新生代和老年代。

  • 新生代:複製算法
  • 老年代:標記-清除算法 或者 標記-整理算法

關於 JVM 堆相關的內容就寫到這裏了,想了解更深更細緻的內容,推薦大家可以去看看 周志明大神寫的 《深入理解Java虛擬機》。


注:如果猿兄這篇博客有任何錯誤和建議,歡迎大家留言,不勝感激!

JVM 系列文章相關推薦:

  1. JVM 體系結構概述
  2. JVM 類加載器類型及類加載機制
  3. JVM 堆內存與垃圾回收
  4. 堆內存調優入門。(暫未更新)
  5. ……(持續更新)
  6. JVM 相關面試題及解答。(暫未更新)

持續更新,點個關注,不再迷路

這裏是 猿兄,爲你分享程序員的世界。

非常感謝各位優秀的程序員們能看到這裏,如果覺得文章還不錯的話,求點贊👍 求關注💗 求分享👬,對我來說真的 非常有用!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章