Java_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主要是針對對象的回收,他們一般位於堆內。那么,方法區中的東西該怎么回收呢?
關於方法區中需要回收的是一些廢棄的常量和無用的類。
- 廢棄的常量的回收。這里看引用計數就可以了。沒有對象引用該常量就可以放心的回收了。
- 無用的類的回收。什么是無用的類呢?
- 該類所有的實例都已經被回收。也就是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區 -- 老年代