堆
我們從前面的文章中知道了,我們創建的對象實例會存放在堆中,也就是對象實例會在堆裏分配內存,所以堆也是垃圾收集的主要區域。但僅僅知道這些是不夠的,今天我們就來具體看一看堆以及堆中的垃圾回收。
這裏在網上找到了一個比較好的堆相關內容的圖,我們就根據這個圖來一步步瞭解堆:
我們可以看到,堆主要分成了兩部分:
- 新生代(Young Generation)
- 老年代(Old Generation)
而 新生代(Young Generation)又可以分成
- Eden 區
- Survivor 0 區
- Survivor 1 區
其中,Eden 區、Survivor 0 區 也被稱爲 From 區和 To 區。(至於爲什麼要這樣分?不要急,後面會說,這裏先有個分區概念就行。)
我們來先看整體再看細節。
整體上看新生代與老年代
整體上可以看到,堆主要分成了兩部分:
- 新生代(Young Generation)
- 老年代(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 異常。
- 整體上我們已經瞭解了,但細節上還有一些問題:
- 爲什麼新生代要劃分成 Eden 區、From 區、To 區?
- 垃圾回收是怎麼進行的?
- 怎麼知道一個對象實例是否該回收?
- … …
帶着這些疑問,我們再從細節上看一下堆的新生代
細節上看新生代
爲什麼新生代要劃分成 Eden 區、From 區、To 區?垃圾回收的過程?
新生代爲什麼這樣劃分,其實我們來了解一下新生代 Minor GC 的過程就知道了(其實也是順便了解一下複製算法)。
Minor GC 過程:複製→清空→互換
複製
首先,當 Eden 區內存滿的時候會觸發第一次 Minor GC,然後把 GC 之後還活着的對象實例拷貝到 From 區;
當 Eden 區再次觸發 GC 的時候,會掃描 Eden 區和 From 區,對這兩個區域進行 GC;經過這次 GC 回收後還存活的對象,則會直接複製到To區域,並且把這些對象的年齡 +1(如果有對象的年齡已經達到了老年的標準,則會複製到老年代區)。清空
第二次及之後的 Minor GC 是把 GC 之後 Eden 區和 From 區還存活的對象複製到 To 區;複製之後,會把 Eden 區和 From 區都清空掉。互換
然後會將此時的清空後的 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 的的對象包含如下幾種:
- 在虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用調用的方法堆棧中使用的參數、局部變量、臨時變量等。
- 在方法區中類靜態變量屬性引用的對象,比如 Java 類的引用類型靜態變量。
- 在方法區中常量引用的對象,比如字符串常量池裏的引用。
- 在本地方法棧中JNI(也就是 Native 方法)引用的對象。
- Java 虛擬機內部的引用,如基本數據類型對應的 Class 對象,一些常駐的異常對象(比如空指針異常,OOM)等,還有系統加載器。
- 所有被同步鎖(synchronized 關鍵字)持有的對象。
- 反映 Java 虛擬機內部清空的 JMXBean、JVMTI 中註冊的回調、本地代碼緩存等。
除了上面這些固定的 GC Roots ,還可以根據用戶所選用的垃圾收集器以及當前回收的內存區域不同,加入其他“臨時性”的對象。
垃圾怎麼回收的?(垃圾回收算法有哪些?)
複製算法
這個算法,我們在講新生代的垃圾回收時,說的就是這個算法。
簡單來說就是:將內存分爲大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完後,將剩下還存活的對象複製到另一塊去,然後再把使用的空間進行一次清理。
好處是:效率高,不會產生大量內存碎片(內存碎片:對象在內存中不連續,這一個那一個,是內存碎片化了)。
壞處是:只使用了一半的內存,浪費了內存。標記-清除算法
這個算法其實就和它的名字一樣 標記-清除:首先將需要回收的對象標記一下,當內存不夠觸發垃圾回收時,就會將所有標記了的對象清除掉。
這個算法很簡單,但是問題也很明顯,因爲標記對象的位置不確定,所以會產生大量內存碎片,並且標記清除過程的效率不高。標記-整理算法
標記-整理算法,也同樣會對需要回收的對象進行標記,但後續不是直接清除標記過的對象,而是讓所有存活對象(也就是未標記對象)全部向內存的一端移動,然後再清理掉端邊界以外的內存。
相比與標記-清除算法,標記-整理算法不會產生內存碎片;但是同樣,效率不高,因爲需要移動大量對象,所以處理效率自然不高。分代收集算法
分代收集算法其實不是什麼新算法,而是將上面說的三種揚長避短,根據不同的情況使用不同的算法。
分代收集算法會根據對象存活週期將內存劃分爲不同的幾個部分,一般就是我們前面說的分成 新生代和老年代。
- 新生代:複製算法
- 老年代:標記-清除算法 或者 標記-整理算法
關於 JVM 堆相關的內容就寫到這裏了,想了解更深更細緻的內容,推薦大家可以去看看 周志明大神寫的 《深入理解Java虛擬機》。
注:如果猿兄這篇博客有任何錯誤和建議,歡迎大家留言,不勝感激!
JVM 系列文章相關推薦:
持續更新,點個關注,不再迷路
這裏是 猿兄,爲你分享程序員的世界。
非常感謝各位優秀的程序員們能看到這裏,如果覺得文章還不錯的話,求點贊👍 求關注💗 求分享👬,對我來說真的 非常有用!!!