深入理解Java-GC機制

今天我們來談談Java主流虛擬機-HotSpot的GC實現機制,本篇文章默認使用HotSpot虛擬機進行介紹,如果沒有特殊說明,其都爲HotSpot虛擬機中的特性。

Java與C++之間有一堵由內存動態分配和垃圾收集技術所圍城的“高牆”,牆外面的人想進去,牆裏面的人卻想出來。

說起垃圾收集,大部分人都把這項技術當做Java語言的伴生產物。事實上,GC的歷史比Java久遠,1960年誕生與MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。關於Garbage Collection的歷史這裏就不多說了,因爲這是一篇技術博客而不是來將歷史的,如果對GC的發展歷史感興趣可以自行百度。

一、GC實現機制-我們爲什麼要去了解GC和內存分配?

說道這個問題,我有一個簡單的回答:在真實工作中的項目中,時不時的會發生內存溢出、內存泄露的問題,這也是不可避免的Bug,這些潛在的Bug在某些時候會影響到項目的正常運行,如果你的項目沒有合理的進行業務內存分配,將會直接影響到的項目的併發處理,當垃圾收集成爲系統達到更高併發量的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節,而瞭解了GC實現機制則是我們一切監控和調節的前提。

二、GC實現機制-Java虛擬機將會在什麼地方進行垃圾回收?

說起垃圾回收的場所,瞭解過JVM(Java Virtual Machine Model)內存模型的朋友應該會很清楚,堆是Java虛擬機進行垃圾回收的主要場所,其次要場所是方法區。

三、GC實現機制-Java虛擬機具體實現流程

在這裏插入圖片描述

我們都知道在Java虛擬機中進行垃圾回收的場所有兩個,一個是堆,一個是方法區。在堆中存儲了Java程序運行時的所有對象信息,而垃圾回收其實就是對那些“死亡的”對象進行其所侵佔的內存的釋放,讓後續對象再能分配到內存,從而完成程序運行的需要。關於何種對象爲死亡對象,在下一部分將做詳細介紹。Java虛擬機將堆內存進行了“分塊處理”,從廣義上講,在堆中進行垃圾回收分爲新生代(Young Generation)和老生代(Old Generation);從細微之處來看,爲了提高Java虛擬機進行垃圾回收的效率,又將新生代分成了三個獨立的區域(這裏的獨立區域只是一個相對的概念,並不是說分成三個區域以後就不再互相聯合工作了),分別爲:Eden區(Eden Region)、From Survivor區(Form Survivor Region)以及To Survivor(To Survivor Region),而Eden區分配的內存較大,其他兩個區較小,每次使用Eden和其中一塊Survivor。Java虛擬機在進行垃圾回收時,將Eden和Survivor中還存活着的對象進行一次性地複製到另一塊Survivor空間上,直到其兩個區域中對象被回收完成,當Survivor空間不夠用時,需要依賴其他老年代的內存進行分配擔保。當另外一塊Survivor中沒有足夠的空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老生代,在老生代中不僅存放着這一種類型的對象,還存放着大對象(需要很多連續的內存的對象),當Java程序運行時,如果遇到大對象將會被直接存放到老生代中,長期存活的對象也會直接進入老年代。如果老生代的空間也被佔滿,當來自新生代的對象再次請求進入老生代時就會報OutOfMemory異常。新生代中的垃圾回收頻率高,且回收的速度也較快。就GC回收機制而言,JVM內存模型中的方法區更被人們傾向的稱爲永久代(Perm Generation),保存在永久代中的對象一般不會被回收。其永久代進行垃圾回收的頻率就較低,速度也較慢。永久代的垃圾收集主要回收廢棄常量和無用類。以String常量abc爲例,當我們聲明瞭此常量,那麼它就會被放到運行時常量池中,如果在常量池中沒有任何對象對abc進行引用,那麼abc這個常量就算是廢棄常量而被回收;判斷一個類是否“無用”,則需同時滿足三個條件:

(1)、該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例;

(2)、加載該類的ClassLoader已經被回收

(3)、該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

虛擬機可以對滿足上述3個條件的無用類進行回收,這裏說的是可以回收而不是必然回收。

大多數情況下,對象在新生代Eden區中分配,當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC;同理,當老年代中沒有足夠的內存空間來存放對象時,虛擬機會發起一次Major GC/Full GC。只要老年代的連續空間大於新生代對象總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full CG。

虛擬機通過一個對象年齡計數器來判定哪些對象放在新生代,哪些對象應該放在老生代。如果對象在Eden出生並經過一次Minor GC後仍然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,並將該對象的年齡設爲1。對象每在Survivor中熬過一次Minor GC,年齡就增加1歲,當他的年齡增加到最大值15時,就將會被晉升到老年代中。虛擬機並不是永遠地要求對象的年齡必須達到MaxTenuringThreshold才能晉升到老年代,如果在Survivor空間中所有相同年齡的對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就可以直接進入老年代,無需等到MaxTenuringThreshold中要求的年齡。

四、GC實現機制-Java虛擬機如何實現垃圾回收機制

(1)、引用計數算法(Reference Counting)

給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器爲0的對象就是不可能再被使用的,這就是引用計數算法的核心。客觀來講,引用計數算法實現簡單,判定效率也很高,在大部分情況下都是一個不錯的算法。但是Java虛擬機並沒有採用這個算法來判斷何種對象爲死亡對象,因爲它很難解決對象之間相互循環引用的問題。

public class ReferenceCountingGC{
  public Object object = null;
  
  private static final int OenM = 1024 * 1024;
  private byte[] bigSize = new byte[2 * OneM];
 
  public static void testCG(){
     ReferenceCountingGC objA = new ReferenceCountingGC(); 
      ReferenceCountingGC objB = new ReferenceCountingGC(); 
      
      objA.object = null;
      objB.object = null;
 
     System.gc();
}
}

在上述代碼段中,objA與objB互相循環引用,沒有結束循環的判斷條件,運行結果顯示Full GC,就說明當Java虛擬機並不是使用引用計數算法來判斷對象是否存活的。
(2)、可達性分析算法(Reachability Analysis)

這是Java虛擬機採用的判定對象是否存活的算法。通過一系列的稱爲“GC Roots"的對象作爲起始點,從這些結點開始向下搜索,搜索所走過的路徑稱爲飲用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的。可作爲GC Roots的對象包括:虛擬機棧中引用的對象、方法區中類靜態屬性引用的對象、方法區中常量引用的對象。本地方法棧JNI引用的對象。
在這裏插入圖片描述

在上圖可以看到GC Roots左邊的對象都有引用鏈相關聯,所以他們不是死亡對象,而在GCRoots右邊有幾個零散的對象沒有引用鏈相關聯,所以他們就會別Java虛擬機判定爲死亡對象而被回收。

五、GC實現機制-何爲死亡對象?

Java虛擬機在進行死亡對象判定時,會經歷兩個過程。如果對象在進行可達性分析後沒有與GC Roots相關聯的引用鏈,則該對象會被JVM進行第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法,如果當前對象沒有覆蓋該方法,或者finalize方法已經被JVM調用過都會被虛擬機判定爲“沒有必要執行”。如果該對象被判定爲沒有必要執行,那麼該對象將會被放置在一個叫做F-Queue的隊列當中,並在稍後由一個虛擬機自動建立的、低優先級的Finalizer線程去執行它,在執行過程中JVM可能不會等待該線程執行完畢,因爲如果一個對象在finalize方法中執行緩慢,或者發生死循環,將很有可能導致F-Queue隊列中其他對象永久處於等待狀態,甚至導致整個內存回收系統崩潰。如果在finalize方法中該對象重新與引用鏈上的任何一個對象建立了關聯,即該對象連上了任何一個對象的引用鏈,例如this關鍵字,那麼該對象就會逃脫垃圾回收系統;如果該對象在finalize方法中沒有與任何一個對象進行關聯操作,那麼該對象會被虛擬機進行第二次標記,該對象就會被垃圾回收系統回收。值得注意的是finaliza方法JVM系統只會自動調用一次,如果對象面臨下一次回收,它的finalize方法不會被再次執行。

六、再探GC實現機制-垃圾收集算法

在這裏插入圖片描述
(1)、標記-清楚算法(Mark-Sweep)

用在老生代中, 先對對象進行標記,然後清楚。標記過程就是第五部分提到的標記過程。值得注意的是,使用該算法清楚過後會產生大量不連續的內存碎片,空間碎片太多可能會導致以後在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。

. (2)、複製算法(Copying)

用在新生代中,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活的對象複製到另外一塊上,然後再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可。

七、空間分配擔保策略-GC過程中的內存擔保機制

當出現大量對象在Minor GC後仍然存活的情況,就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。與生活中的銀行貸款類似,老年代要進行這樣的擔保,前提是老年代本身還有容納這些對象的剩餘空間,一共有多少對象會存活下來在實際完後才能內存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作爲經驗值,與老年代的剩餘空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。如果出現擔保失敗,就只好重新發起一次Full GC來進行內存的分配。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章