1. 程式人生 > >面試必備 | 深入解析Java垃圾回收機制

面試必備 | 深入解析Java垃圾回收機制

引入垃圾回收

程式計數器、 虛擬機器棧、 本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。 每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的(儘管在執行期會由JIT編譯器
進行一些優化,但在本章基於概念模型的討論中,大體上可以認為是編譯期可知的),因此這幾個區域的記憶體分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因為方法結束或者執行緒結束時,記憶體自然就跟隨著回收了。 而Java堆和方法區則不一樣,一個介面中的多個實現類需要的記憶體可能不一樣,一個方法中的多個分支需要的記憶體也可能不一樣,我們只有在程式處於執行期間時才能知道會建立哪些物件,這部分記憶體的分配和回收都是動態的,垃圾收集器所關注的是這部分記憶體——-《深入理解Java虛擬機器》

自動垃圾回收機制就是尋找Java堆中的物件,並對物件進行分類判別,尋找出正在使用的物件和已經不會使用的物件,然後把那些不會使用的物件從堆上清除。
自動垃圾回收機制就是要解決三個問題:

  • 哪些記憶體需要回收?

  • 什麼時候回收?

  • 如何回收?

哪些記憶體需要回收?

引用計數法

對於第一個問題,也就是判斷是否還需要使用,最簡單的方法就是通過目前是否有引用指向這個物件,如果沒有就說明這個物件不會再被使用了,如果有就說明這個物件可能還會繼續被使用,這種通過引用是否存在的方法就叫做引用計數法,但這個方法存在一個問題就是無法解決物件迴圈引用的問題,因此又出現了可達性分析的方法來判斷物件是否可以被會回收。

可達性分析

這個演算法的基本思路就是通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個物件不可達)時,則證明此物件是不可用的。
在Java語言中,可作為GC Roots的物件包括下面幾種:

  • 虛擬機器棧(棧幀中的本地變量表)中引用的物件。

  • 方法區中類靜態屬性引用的物件。

  • 方法區中常量引用的物件。

  • 本地方法棧中JNI(即一般說的Native方法)引用的物件。

如何回收

垃圾收集器通常會幫我們在後臺自動進行垃圾回收。關於具體的回收過程只要有以下這些步驟

  • Step 1: Marking 標記

第一步就是標記,也就是垃圾收集器會找出那些需要回收的物件所在的記憶體和不需要回收的物件所在的記憶體,並把它們標記出來,簡單的說,也就是先找出垃圾在哪

所有堆中的物件都會被掃描一遍,以此來確定回收的物件,所以這通常會是一個相對比較耗時的過程

  • Step 2: Normal Deletion

垃圾收集器會清除掉上一步標記出來的那些需要回收的物件區域

存在的問題就是碎片問題:
標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程
序執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

  • Step 2a: Deletion with Compacting 壓縮

由於簡單的清除可能會存在碎片的問題,所以又出現了壓縮清除的方法,也就是先清除需要回收的物件,然後再對記憶體進行壓縮操作,將記憶體分成可用和不可用兩大部分

為什麼需要分代收集?

就像前文所述,標記物件和壓縮記憶體的過程在JVM中是不高效的,分配的物件越多,垃圾收集的時間就越長。但是,經過一些經驗型性的統計分析表明,一個程式中大部分物件都是短命的!

下圖就是一個類似的統計資料,縱座標表示分配物件所佔用的記憶體大小,橫座標表示自分配物件過去的時間


從圖中我們看到,大部分物件沒活多久就死了,存活較久的只是少類物件

JVM的分代

為了增大垃圾收集的效率,所以JVM將堆進行分代,分為不同的部分,一般有三部分,新生代,老年代和永久代

新生代

所有新new出來的物件都會最先出現在新生代中,當新生代這部分記憶體滿了之後,就會發起一次垃圾收集事件,這種發生在新生代的垃圾收集稱為Minor collections。這種收集通常比較快,因為新生代的大部分物件都是需要回收的,那些暫時無法回收的就會被移動到老年代。

Stop the World事件-所有minor garbage collections都是Stop the World事件,也就是意味著所有的應用執行緒都需要停止,直到垃圾回收的操作全部完成。類似於
“你媽媽在給你打掃房間的時候,肯定也會讓你老老實實地在椅子上或者房間外待著,如果她一邊打掃,你一邊亂扔紙屑,這房間還能打掃完?”

老年代

老年代用來儲存那些存活時間較長的物件。一般來說,我們會給新生代的物件限定一個存活的時間,當達到這個時間還沒有被收集的時候就會被移動到老年代中。老年代區域的垃圾收集叫做major garbage collection

Major garbage collection也是一個Stop the World事件。通常Major garbage collection都相對比較慢,因為老年代的收集包括了對所有物件的收集,也就是同時需要收集新生代和老年代的物件。

永久代

The Permanent generation contains metadata required by the JVM to describe the classes and methods used in the application. The permanent generation is populated by the JVM at runtime based on classes in use by the application. In addition, Java SE library classes and methods may be stored here.

Classes may get collected (unloaded) if the JVM finds they are no longer needed and space may be needed for other classes. The permanent generation is included in a full garbage collection.

分代垃圾收集過程詳述

我們已經知道垃圾回收所需要的方法和堆記憶體的分代,那麼接下來我們就來具體看一下垃圾回收的具體過程

  • 第一步 所有new出來的物件都會最先分配到新生代區域中,兩個survivor區域初始化是為空的

  • 第二步,當eden區域滿了之後,就引發一次 minor garbage collection

  • 第三步,當在minor garbage collection,存活下來的物件就會被移動到S0survivor區域

  • 第四步,然後當eden區域又填滿的時候,又會發生下一次的垃圾回收,存活的物件會被移動到survivor區域而未存活物件會被直接刪除。但是,不同的是,在這次的垃圾回收中,存活物件和之前的survivor中的物件都會被移動到s1中。一旦所有物件都被移動到s1中,那麼s2中的物件就會被清除,仔細觀察圖中的物件,數字表示經歷的垃圾收集的次數。目前我們已經有不同的年齡物件了。

  • 第五步,下一次垃圾回收的時候,又會重複上次的步驟,清除需要回收的物件,並且又切換一次survivor區域,所有存活的物件都被移動至s0。eden和s1區域被清除。

  • 第六步,重複以上步驟,並記錄物件的年齡,當有物件的年齡到達一定的閾值的時候,就將新生代中的物件移動到老年代中。在本例中,這個閾值為8.

  • 第七步,接下來垃圾收集器就會重複以上步驟,不斷的進行物件的清除和年代的移動

  • 最後,我們觀察上述過程可以發現,大部分的垃圾收集過程都是在新生代進行的,直到老年代中的記憶體不夠用了才會發起一次 major GC,會進行標記和整理壓縮。