一、Java垃圾回收機制

在java中,程式設計師是不需要顯示的去釋放一個物件的記憶體的,而是由虛擬機器自行執行。在JVM中,有一個垃圾回收執行緒,它是低優先順序的,在正常情況下是不會執行的,只有在虛擬機器空閒或者當前堆記憶體不足時,才會觸發執行,掃描那些沒有被任何引用的物件,並將它們新增到要回收的集合中,進行回收。

二、GC是什麼?為什麼要GC

GC 垃圾收集(Gabage Collection),記憶體處理是程式設計人員容易出現問題的地方,忘記或者錯誤的記憶體。

不當的回收可能會導致程式或系統的不穩定甚至崩潰,Java 提供的 GC 功能可以自動監測物件是否超過作用域從而達到自動回收記憶體的目的,Java 語言沒有提供釋放已分配記憶體的顯示操作方法。

對於GC來說,當程式設計師建立物件時,GC就開始監控這個物件的地址、大小以及使用情況。

通常,GC採用有向圖的方式記錄和管理堆(heap)中的所有物件。透過這種方式確定哪些物件是“可達的”,哪些物件是“不可達的”。當GC確定一些物件為“不可達”時,GC就有責任回收這些記憶體空間。

程式設計師可以手動執行System。gc(),通知GC執行,但是Java語言規範並不保證GC一定會執行。

JVM的垃圾回收機制——垃圾回收演算法

JVM GC

三、Java 中的引用型別

強引用:發生 gc 的時候不會被回收。

軟引用:有用但不是必須的物件,在發生記憶體溢位之前會被回收。

弱引用:有用但不是必須的物件,在下一次GC時會被回收。

虛引用(幽靈引用/幻影引用):無法透過虛引用獲得物件,用 PhantomReference 實現虛引用,虛引用的用途是在 gc 時返回一個通知。

四、如何判斷物件是否可以被回收?什麼時候被回收?

垃圾收集器在做垃圾回收的時候,首先需要判定的就是哪些記憶體是需要被回收的,哪些物件是存活的,是不可以被回收的;哪些物件已經死亡了,需要被回收。

一般有兩種方法來判斷:

引用計數器法

:為每個物件建立一個引用計數,有物件引用時計數器 +1,引用被釋放時計數 -1,當計數器為 0 時就可以被回收。但是他有一個缺點是不能解決迴圈引用的問題。

可達性分析演算法

:從 GC Roots 開始向下搜尋,搜尋所走過的路徑稱為引用鏈。當一個物件到 GC Roots 沒有任何引用鏈相連時,則證明此物件是可以被回收的。

當物件對當前使用這個物件的應用程式變得不可觸及的時候,這個物件就可以被回收了。

垃圾回收不會發生在永久代,如果永久代滿了或者是超過了臨界值,會觸發完全垃圾回收(Full GC)。檢視垃圾收集器的輸出資訊,就會發現永久代也是被回收的。這就是為什麼正確的永久代大小對避免Full GC是非常重要的原因。

五、JVM 的垃圾回收演算法?

1.標記-清除演算法

:標記無用物件,然後進行清除回收。缺點:效率不高,無法清除垃圾碎片。

該演算法分為兩個階段,

標記和清除

。標記階段標記所有需要回收的物件,清除階段回收被標記的物件所佔用的空間。該演算法最大的問題就是記憶體碎片嚴重化,後續可能發生物件不能找到利用空間的問題。

JVM的垃圾回收機制——垃圾回收演算法

標記清除演算法

2.複製演算法

:按照容量劃分二個大小相等的記憶體區域,當一塊用完的時候將活著的物件複製到另一塊上,然後再把已使用的記憶體空間一次清理掉。缺點:記憶體使用率不高,只有原來的一半。

按記憶體容量將記憶體劃分為等大小的兩塊。每次只使用其中一塊,當這一塊記憶體滿後將尚存活的物件複製到另一塊上去,把已使用的記憶體清掉。

JVM的垃圾回收機制——垃圾回收演算法

複製演算法

3.標記-整理演算法

:標記無用物件,讓所有存活的物件都向一端移動,然後直接清除掉端邊界以外的記憶體。

標記後不是清理物件,而是將存活物件移向記憶體的一端。然後清除端邊界外的物件。

JVM的垃圾回收機制——垃圾回收演算法

標記整理演算法

4.分代演算法

:根據物件存活週期的不同將記憶體劃分為幾塊,一般是新生代和老年代,新生代基本採用複製演算法,老年代採用標記整理演算法。

當前商業虛擬機器都採用分代收集的垃圾收集演算法。分代收集演算法,顧名思義是根據物件的存活週期將記憶體劃分為幾塊。一般包括年輕代、老年代 和 永久代。

JVM的垃圾回收機制——垃圾回收演算法

分代收集

六、垃圾收集器

Java 堆記憶體被劃分為新生代和年老代兩部分,新生代主要使用複製和標記-清除垃圾回收演算法;

年老代主要使用標記-整理垃圾回收演算法,因此 java 虛擬中針對新生代和年老代分別提供了多種不同的垃圾收集器,JDK1。6 中 Sun HotSpot 虛擬機器的垃圾收集器如下:

JVM的垃圾回收機制——垃圾回收演算法

垃圾收集器

其中用於回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,

回收老年代的收集器包括Serial Old、Parallel Old、CMS,

有用於回收整個Java堆的G1收集器。

新生代垃圾回收器一般採用的是複製演算法,複製演算法的優點是效率高,缺點是記憶體利用率低;老年代回收器一般採用的是標記-整理的演算法進行垃圾回收。

Serial收集器(複製演算法): 新生代單執行緒收集器,標記和清理都是單執行緒,優點是簡單高效;

ParNew收集器 (複製演算法): 新生代收並行集器,實際上是Serial收集器的多執行緒版本,在多核CPU環境下有著比Serial更好的表現;

Parallel Scavenge收集器 (複製演算法): 新生代並行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 使用者執行緒時間/(使用者執行緒時間+GC執行緒時間),高吞吐量可以高效率的利用CPU時間,儘快完成程式的運算任務,適合後臺應用等對互動相應要求不高的場景;

Serial Old收集器 (標記-整理演算法): 老年代單執行緒收集器,Serial收集器的老年代版本;

Parallel Old收集器 (標記-整理演算法): 老年代並行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本;

CMS(Concurrent Mark Sweep)收集器(標記-清除演算法): 老年代並行收集器,以獲取最短回收停頓時間為目標的收集器,具有高併發、低停頓的特點,追求最短GC回收停頓時間。

G1(Garbage First)收集器 (標記-整理演算法): Java堆並行收集器,G1收集器是JDK1。7提供的一個新收集器,G1收集器基於“標記-整理”演算法實現,也就是說不會產生記憶體碎片。此外,G1收集器不同於之前的收集器的一個重要特點是:G1回收的範圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的範圍僅限於新生代或老年代。

CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器。對於要求伺服器響應速度的應用上,這種垃圾回收器非常適合。在啟動 JVM 的引數加上 “-XX:+UseConcMarkSweepGC” 來指定使用 CMS 垃圾回收器。

CMS 使用的是標記-清除的演算法實現的,所以在 gc 的時候回產生大量的記憶體碎片,當剩餘記憶體不能滿足程式執行要求時,系統將會出現 Concurrent Mode Failure,臨時 CMS 會採用 Serial Old 回收器進行垃圾清除,此時的效能將會被降低。

七、分代垃圾回收器的工作機制?

舉個栗子:

Java物件的一生:

我是一個java物件,我出生在Eden區,在Eden區有一些跟我一樣的兄弟們,我們在Eden區中一起玩,每天都有新的兄弟進來。有一天Eden區中的人實在是太多了,我就被迫去了Survivor區的“From”區,自從去了Survivor區,在這裡生活非常不穩定。有時候在Survivor的“From”區,有時候在Survivor的“To”區,居無定所。直到我15歲的時候(預設15歲),就被分配到年老代那邊,在這裡人很多,並且年齡都挺大的。在年老代裡,我生活了很久,每次GC年齡就+1,然後被回收。

分代回收器有兩個分割槽:

老生代和新生代

,新生代預設的空間佔比總空間的 1/3,老生代的預設佔比是 2/3。

新生代使用的是複製演算法,新生代裡有 3 個分割槽:Eden、To Survivor、From Survivor,它們的預設佔比是

8:1:1

,它的執行流程如下:

在GC開始的時候,物件只會存在於Eden區和名為“From”的Survivor區,Survivor區“To”是空的。緊接著進行GC,Eden區中所有存活的物件都會被複制到“To Survivor區”,仍存活的物件會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以透過-XX:MaxTenuringThreshold來設定)的物件會被移動到年老代中。

清空 Eden 和 From Survivor 分割槽;

這時From Survivor 和 To Survivor 分割槽會互換角色,分割槽交換,From Survivor 變 To Survivor,To Survivor 變 From Survivor。

每次在 From Survivor 到 To Survivor 移動時都存活的物件,年齡就 +1,當年齡到達 15(預設配置是 15)時,升級為老生代。大物件也會直接進入老生代。

老生代當空間佔用到達某個值之後就會觸發全域性垃圾收回,一般使用標記整理的執行演算法。以上這些迴圈往復就構成了整個分代垃圾回收的整體執行流程。

JVM的垃圾回收機制——垃圾回收演算法

物件優先在 Eden 區分配:

多數情況,物件都在新生代 Eden 區分配。當 Eden 區分配沒有足夠的空間進行分配時,虛擬機器將會發起一次 Minor GC。如果本次 GC 後還是沒有足夠的空間,則將啟用分配擔保機制在老年代中分配記憶體。

Minor GC

是指發生在新生代的 GC,因為 Java 物件大多都是朝生夕死,所有 Minor GC 非常頻繁,一般回收速度也非常快;

Major GC/Full GC

是指發生在老年代的 GC,出現了 Major GC 通常會伴隨至少一次 Minor GC。Major GC 的速度通常會比 Minor GC 慢 10 倍以上。

大物件直接進入老年代:

新生代使用的是標記-清除演算法來處理垃圾回收的,如果大物件直接在新生代分配就會導致 Eden 區和兩個 Survivor 區之間發生大量的記憶體複製。因此對於大物件都會直接在老年代進行分配。

所謂大物件是指,需要大量連續記憶體空間的Java物件,最典型的大物件就是那種很長的字串以及陣列。大物件對虛擬機器的記憶體分配來說就是一個壞訊息,經常出現大物件容易導致記憶體還有不少空間時就提前觸發垃圾收集以獲取足夠的連續空間來 “安置” 它們。

虛擬機器提供了一個

XX:PretenureSizeThreshold

引數,令大於這個設定值的物件直接在老年代分配,這樣做的目的是避免在Eden區及兩個Survivor區之間發生大量的記憶體複製(新生代採用的是複製演算法)。

長期存活物件將進入老年代:

虛擬機器採用分代收集的思想來管理記憶體,那麼記憶體回收時就必須判斷哪些物件應該放在新生代,哪些物件應該放在老年代。因此虛擬機器給每個物件定義了一個物件年齡的計數器,如果物件在 Eden 區出生,並且能夠被 Survivor 容納,將被移動到 Survivor 空間中,這時設定物件年齡為 1。物件在 Survivor 區中每過一次 Minor GC 年齡就加 1,當年齡達到一定程度(預設 15) 就會被晉升到老年代。