1. 程式人生 > >GC和垃圾收集器

GC和垃圾收集器

Java —— GC

標籤(空格分隔): Java

要想深入瞭解Java的GC(Garbage Collection),我們應該先探尋如下三個問題:

  • What? -- 哪些記憶體需要回收?
  • When? -- 什麼時候回收?
  • How? -- 如何回收?

GC Definition

Definition: Program itself finds and collects memory which is useless. It is a form of automatic memory management which doesn't need programmers release memory. Java中為什麼會有GC機制呢?

  • 安全性考慮;-- for security.
  • 減少記憶體洩露;-- erase memory leak in some degree.
  • 減少程式設計師工作量。-- Programmers don't worry about memory releasing.

What? -- 哪些記憶體需要回收?

我們知道,記憶體執行時JVM會有一個執行時資料區來管理記憶體。它主要包括5大部分:程式計數器(Program Counter Register)、虛擬機器棧(VM Stack)、本地方法棧(Native Method Stack)、方法區(Method Area)、堆(Heap).

而其中程式計數器、虛擬機器棧、本地方法棧

是每個執行緒私有的記憶體空間,隨執行緒而生,隨執行緒而亡。例如棧中每一個棧幀中分配多少記憶體基本上在類結構去誒是哪個下來時就已知了,因此這3個區域的記憶體分配和回收都是確定的,無需考慮記憶體回收的問題。

方法區和堆就不同了,一個介面的多個實現類需要的記憶體可能不一樣,我們只有在程式執行期間才會知道會建立哪些物件,這部分記憶體的分配和回收都是動態的,GC主要關注的是這部分記憶體。

總而言之,GC主要進行回收的記憶體是JVM中的方法區; 涉及到多執行緒(指堆)、多個對該物件不同型別的引用(指方法區),才會涉及GC的回收。

When? -- 什麼時候回收?

在面試中經常會碰到這樣一個問題(事實上筆者也碰到過):如何判斷一個物件已經死去?

很容易想到的一個答案是:對一個物件新增引用計數器。每當有地方引用它時,計數器值加1;當引用失效時,計數器值減1.而當計數器的值為0時這個物件就不會再被使用,判斷為已死。是不是簡單又直觀。然而,很遺憾。這種做法是錯誤的!(面試時可千萬別這樣回答哦,我就是不假思索這樣回答,然後就。。)為什麼是錯的呢?事實上,用引用計數法確實在大部分情況下是一個不錯的解決方案,而在實際的應用中也有不少案例,但它卻無法解決物件之間的迴圈引用問題。比如物件A中有一個欄位指向了物件B,而物件B中也有一個欄位指向了物件A,而事實上他們倆都不再使用,但計數器的值永遠都不可能為0,也就不會被回收,然後就發生了記憶體洩露。。

所以,正確的做法應該是怎樣呢? 在Java,C#等語言中,比較主流的判定一個物件已死的方法是:可達性分析(Reachability Analysis). 所有生成的物件都是一個稱為"GC Roots"的根的子樹。從GC Roots開始向下搜尋,搜尋所經過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈可以到達時,就稱這個物件是不可達的(不可引用的),也就是可以被GC回收了。如下圖所示:

[可達性演算法判定物件是否可回收][1]

無論是引用計數器還是可達性分析,判定物件是否存活都與引用有關!那麼,如何定義物件的引用呢?

我們希望給出這樣一類描述:當記憶體空間還夠時,能夠儲存在記憶體中;如果進行了垃圾回收之後記憶體空間仍舊非常緊張,則可以拋棄這些物件。所以根據不同的需求,給出如下四種引用,根據引用型別的不同,GC回收時也會有不同的操作:

  • 強引用(Strong Reference):Object obj = new Object();只要強引用還存在,GC永遠不會回收掉被引用的物件。
  • 軟引用(Soft Reference):描述一些還有用但非必需的物件。在系統將會發生記憶體溢位之前,會把這些物件列入回收範圍進行二次回收(即系統將會發生記憶體溢位了,才會對他們進行回收。)
  • 弱引用(Weak Reference):程度比軟引用還要弱一些。這些物件只能生存到下次GC之前。當GC工作時,無論記憶體是否足夠都會將其回收(即只要進行GC,就會對他們進行回收。)
  • 虛引用(Phantom Reference):一個物件是否存在虛引用,完全不會對其生存時間構成影響。

方法區

What部分我們已經提到,GC主要回收的是堆和方法區中的記憶體,而上面的How主要是針對物件的回收,他們一般位於堆內。那麼,方法區中的東西該怎麼回收呢?

關於方法區中需要回收的是一些廢棄的常量無用的類

  1. 廢棄的常量的回收。這裡看引用計數就可以了。沒有物件引用該常量就可以放心的回收了。
  2. 無用的類的回收。什麼是無用的類呢?
  • 該類所有的例項都已經被回收。也就是Java堆中不存在該類的任何例項;
  • 載入該類的ClassLoader已經被回收;
  • 該類對應的java.lang.Class物件沒有任何地方被引用,無法在任何地方通過反射訪問該類的方法。

總而言之,對於堆中的物件,主要用可達性分析判斷一個物件是否還存在引用,如果該物件沒有任何引用就應該被回收。而根據我們實際對引用的不同需求,又分成了4中引用,每種引用的回收機制也是不同的。 對於方法區中的常量和類,當一個常量沒有任何物件引用它,它就可以被回收了。而對於類,如果可以判定它為無用類,就可以被回收了。

How? -- 如何回收?

標記-清除(Mark-Sweep)演算法

分為兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。 缺點:效率問題,標記和清除兩個過程的效率都不高;空間問題,會產生很多碎片。

複製演算法

將可用記憶體按容量劃分為大小相等的兩塊,每次只用其中一塊。當這一塊用完了,就將還存活的物件複製到另外一塊上面,然後把原始空間全部回收。高效、簡單。 缺點:將記憶體縮小為原來的一半。

標記-整理(Mark-Compat)演算法

標記過程與標記-清除演算法過程一樣,但後面不是簡單的清除,而是讓所有存活的物件都向一端移動,然後直接清除掉端邊界以外的記憶體。

分代收集(Generational Collection)演算法

  • 新生代中,每次垃圾收集時都有大批物件死去,只有少量存活,就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集;
  • 老年代中,其存活率較高、沒有額外空間對它進行分配擔保,就應該使用“標記-整理”或“標記-清理”演算法進行回收。

一些收集器

Serial收集器

單執行緒收集器,表示在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。"Stop The World".

ParNew收集器

實際就是Serial收集器的多執行緒版本。

  • 併發(Parallel):指多條垃圾收集執行緒並行工作,但此時使用者執行緒仍然處於等待狀態;
  • 並行(Concurrent):指使用者執行緒與垃圾收集執行緒同時執行,使用者程式在繼續執行,而垃圾收集程式運行於另一個CPU上。

Parallel Scavenge收集器

該收集器比較關注吞吐量(Throughout)(CPU用於使用者程式碼的時間與CPU總消耗時間的比值),保證吞吐量在一個可控的範圍內。

CMS(Concurrent Mark Sweep)收集器

CMS收集器是一種以獲得最短停頓時間為目標的收集器。

G1(Garbage First)收集器

從JDK1.7 Update 14之後的HotSpot虛擬機器正式提供了商用的G1收集器,與其他收集器相比,它具有如下優點:並行與併發;分代收集;空間整合;可預測的停頓等。

本部分主要分析了三種不同的垃圾回收演算法:Mark-Sweep, Copy, Mark-Compact. 每種演算法都有不同的優缺點,也有不同的適用範圍。而JVM中對垃圾回收器並沒有嚴格的要求,不同的收集器會結合多個演算法進行垃圾回收。

記憶體分配

Java技術體系中所提倡的自動記憶體管理最終可以歸結為自動化的解決2個問題:給物件分配記憶體以及回收分配給物件的記憶體

物件優先在Eden分配

大多數情況下,物件在新生代Eden區分配。當Eden區沒有足夠的記憶體時,虛擬機器將發起一次Minor GC。

  • Minor GC(新生代GC):指發生在新生代的垃圾收集動作,因為Java物件大多都具備朝生夕滅的特性,所以Minor GC發生的非常頻繁。
  • Full GC/Major GC(老年代GC):指發生在老年代的GC,出現了Major GC,經常會伴隨至少一次的Minor GC。

大物件直接進老年代

大物件是指需要大量連續記憶體空間的Java物件(例如很長的字串以及陣列)。

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

JVM為每個物件定義一個物件年齡計數器。

  • 如果物件在Eden出生並經歷過第一次Minor GC後仍然存活,並且能夠被Survivor容納,則應該被移動到Survivor空間中,並且年齡物件設定為1;
  • 物件在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度(預設為15歲,可通過引數-XX:MaxTenuringThreshold設定),就會被晉升到老年代中。
  • 要注意的是:JVM並不是永遠的要求物件的年齡必須達到MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一般,年齡大於等於該年齡的物件就可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。

空間分配擔保

  • 在發生Minor GC之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,則進行Minor GC是安全的;
  • 如果不成立,則虛擬機器會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果允許,則急促檢查老年代最大可用連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試著進行一次Minor GC,儘管它是有風險的;
  • 如果小於或者HandePromotionFailure設定為不允許冒險,則這時要改為進行一次Full GC.

總結

本篇部落格主要根據Java的GC原理,從What,When,How三方面對如何進行垃圾回收做了分析。 簡而言之: What -- 堆和方法區; When -- 已死的物件(引用無法可達); How -- 標記-清除-整理-複製演算法。 關於GC問題,牢牢把握住這三個問題,然後進行發散性思維,便可以很好的掌握這部分內容了。 最後對Java對物件的記憶體分配策略進行了介紹:新生代Eden區 -- Survivor區 -- 老年代