本文共3198字,是本人前幾天面試被提問到的一個問題,將在該文中闡述關於Java垃圾回收——Full GC的相關知識,包括定義、觸發條件、具體過程。
前幾天面試的時候,面試官在最後問了我一個有關Full GC的垃圾回收問題,希望我說明下該GC的定義、以及是如何觸發的問題,有感便記下這篇文章。
垃圾回收的定義
首先我們要知道GC這個概念,即就是我們常說的Java垃圾回收機制,Java通過可達性分析一個對象的引用是否存在,當不存在的時候,將回收堆中的對象。
目前主流的JVM,也就是HotSpot,採用的是分代收集算法。分代的含義涉及到Java的運行時數據區域,在下面幾個環節我們來簡單描述下Java的內存結構、FullGC的觸發以及分代收集算法是對應的哪些內存區域。
垃圾回收相關基礎知識
一般而言,GC主要是針對運行的數據區的。作為程式設計師要關注的區域主要有5塊,分別是方法區(Method Area),Java棧(Java stack),本地方法棧(Native Method Stack),堆(Heap),程序計數器(Program Counter Register)。實際jvm在管理內存的時候,比這個分的更細緻,只不過做應用程式開發,我們只需要關注這5塊就可以了。
可以關注到,其中有一個區域叫做「堆」,也就是Heap了,它是Jvm管理的內存中最大的一塊。程序的主要數據也都是存放在堆內存中的,也就是說程序所創建的對象基本上都在該區域進行內存分配,這一塊區域被所有的線程所共享,通常出現線程安全問題的一般都是這個區域的數據出現的問題。通常我們所說的gc主要是針對java heap這塊區域的。下面來了解一下heap區。
可見,Heap區在設計上是分代設計的,其劃分為了Eden、Survivor 和 Tenured/Old ,其中Eden區、Survivor(存活)屬於年輕代,Tenured/Old區屬於老年代或者持久代。
一般我們將年輕代發生的GC稱為Minor GC,對老年代進行GC稱為Major GC。
而本文所要闡述的FullGC是對整個堆來說的,在最近幾個版本的JDK裡默認包括了對永生帶即方法區的回收(JDK8中無永生帶了),出現Full GC的時候經常伴隨至少一次的Minor GC,但非絕對的。Major GC的速度一般會比Minor GC慢10倍以上。
到這裡,可能會有些人對heap分區的意義不太理解,我們在這裡就簡單介紹下年輕代和老年代是如何進行工作的,我們需要知道關於JVM的堆區對象分配的一般規則:
1. 對象優先在Eden區分配
2. 大對象直接進入老年代(-XX:PretenureSizeThreshold=3145728 這個參數來定義多大的對象直接進入老年代)
3. 長期存活的對象將進入老年代(在JDK8中測試,-XX:MaxTenuringThreshold=1的閥值設定根本沒用)
4. 動態對象年齡判定(虛擬機並不會永遠地要求對象的年齡都必須達到MaxTenuringThreshold才能晉升老年代,如果Survivor空間中相同年齡的所有對象的大小總和大於Survivor的一半,年齡大於或等於該年齡的對象就可以直接進入老年代)
5. 空間分配擔保
6. 只要老年代的連續空間大於(新生代所有對象的總大小或者歷次晉升的平均大小)就會進行minor GC,否則會進行full GC。
FullGC的觸發條件
在了解了關於GC的一些相關知識後,我們需要知道FullGC的觸發條件(這個是主要提問到點)。
上個環節我們知道了,FullGC是針對整個Heap區而言的,它將在以下幾種情況被觸發:
在程序中調用了System.gc()方法。此方法的調用是建議JVM進行Full GC,雖然只是建議而非一定,但很多情況下它會觸發 Full GC,從而增加Full GC的頻率,也即增加了間歇性停頓的次數。強烈影響系建議能不使用此方法就別使用,讓虛擬機自己去管理它的內存,可通過通過-XX:+ DisableExplicitGC來禁止RMI(Java遠程方法調用)調用System.gc。老年代空間不足。老年代空間只有在新生代對象轉入及創建為大對象、大數組時才會出現不足的現象,當執行Full GC後空間仍然不足,則拋出如下錯誤【java.lang.OutOfMemoryError: Java heap space】,而為避免以上兩種狀況引起的Full GC,調優時應儘量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組。Permanet Generation空間滿了。也就是以前所說的方法區,Permanet Generation中存放的為一些class的信息等,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置為採用CMS GC的情況下會執行Full GC。如果經過Full GC仍然回收不了,那麼JVM會拋出錯誤信息:java.lang.OutOfMemoryError: PermGen space 。為避免Perm Gen佔滿造成Full GC現象,可採用的方法為增大Perm Gen空間或轉為使用CMS GC。通過Minor GC後進入老年代的平均大小大於老年代的可用內存。如果發現統計數據說之前Minor GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發Minor GC而是轉為觸發full GC。由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小。分配很大的對象。所謂大對象,是指需要大量連續內存空間的java對象,例如很長的數組,此種對象會直接進入老年代,而老年代雖然有很大的剩餘空間,但是無法找到足夠大的連續空間來分配給當前對象,此種情況就會觸發JVM進行Full GC。為了解決這個問題,CMS垃圾收集器提供了一個可配置的參數,即-XX:+UseCMSCompactAtFullCollection開關參數,用於在「享受」完Full GC服務之後額外免費贈送一個碎片整理的過程,內存整理的過程無法並發的,空間碎片問題沒有了,但停頓時間不得不變長了,JVM設計者們還提供了另外一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數用於設置在執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的。常見的垃圾回收器——回收過程
垃圾回收器並不是本文的主題,但還是想簡單列舉一下:
Serial收集器ParNew收集器Parallel Scavenge收集器CMS(Concurrent Mark Sweep)收集器G1(Garbage First)收集器(從JDK1.7 Update 14之後的HotSpot虛擬機正式提供了商用的G1收集器)
不知道各位還好不好奇上上個環節說到的分代收集算法。
大致我們也能猜到,其實就是針對年輕代和老年代,JVM將使用不同的垃圾收集算法進行收集,達到高效的垃圾回收。
年輕代採用的是標記-複製算法,將需要回收的對象標記,將不需要的對象移動到Survivor空間,然後將標記對象回收,該算法可以實現對大多數會失效的對象進行回收,對少部分不需要回收的對象進行轉移,保證eden區擁有連續的內存空間,而且複製的效率高。
因為在年輕代不需要回收的對象一般是很少的,每次垃圾收集時都有大批對象死去,只有少量存活,選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。
老年代採用的是標記-整理算法,將需要回收的對象標記,將不需要的對象進行移動整理,使不需要回收的對象佔用連續的內存空間,再清除回收對象,保證老年代擁有連續的內存空間,而且整理效率高。
因為在老年代需要回收的對象一般是很少的,其存活率較高、沒有額外空間對它進行分配擔保。