這篇文章將為大家詳細講解有關深入淺析JVM中的垃圾收集器,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
說起垃圾收集(Garbage Collection,GC),大部分人都把這項技術當做Java語言的伴生產物。事實上,GC的歷史遠比Java久遠,1960年誕生于MIT的Lisp是第一門真正使用內存動態分配和垃圾收集技術的語言。當List還在胚胎時期時,人們就在思考GC需要完成的3件事情:
一、哪些內存需要回收?
從JVM區域結構看,可將這些區域劃分為“靜態內存”和“動態內存”兩類。程序計數器、虛擬機棧、本地方法3個區域是“靜態”的,因為這幾個區域的內存分配和回收都具備確定性,都隨著線程而生,隨著線程而滅。但Java堆和方法區不一樣,內存分配都存在不確定性,只有在程序處于運行期間才能知道會創建哪些對象,這部分內存和回收都是動態的,垃圾收集器所關注的是這部分內存。
在堆里面存放著Java世界幾乎所有的對象實例,垃圾回收器在對堆進行回收前,第一件事情就是就是要確定這些對象哪些還"存活"著,哪些已經"死去"。那么又怎么確定對象已經"死去"呢?
1.引用計數法:
分配對象時給對象添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是沒有再被使用了??陀^地說,引用計數法(Reference Counting)的實現簡單,判斷效率也很高,但是在主流的Java虛擬機里面沒有選用引用計數法來管理內存,其中最主要的原因是它很難解決對象之間相互循環引用的問題。例如:
public class ReferenceCountingGC { public Object instance = null; private byte[] bigsize = new byte[2*1024*1024]; public static void testGC(){ ReferenceCountingGC objA = new ReferenceCountingGC(); ReferenceCountingGC objB = new ReferenceCountingGC(); objA.instance = objB; objB.instance = objA; objA = null; objB = null; System.gc(); } }
當設置objA = null;objB = null后這兩個對象再無任何引用,實際上這兩個對象已經不可能再被訪問,但是它們因為互相引用著對方,導致它們的引用計數都不為0,于是引用計數算法無法通知GC收集器回收它們。如果這個對象特別大,則會造成嚴重的內存泄露。
2.可達性分析算法:
可達性分析(Reachability Analysis)的基本思想是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時(也就是GC Roots到這個對象不可達),則證明此對象是不可用的。如下圖所示:
對象Object5、Object6、Object7相互雖然有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的對象。在Java語言中,可作為GC Roots的對象包括下面幾種:
二、什么時候回收?
虛擬機為了分析GC Roots這項工作必須在一個能確保一致性的快照中進行,這里的“一致性”的意思就是指在整個分析期間整個執行系統看起來就像被凍結在某個時間點上——這叫安全點。當然,程序執行時并非在所有地方都能停頓下來開始GC,只有到達安全點時才能暫停。安全點選址也有規定的,選定基本上是以程序“是否具有讓程序長時間執行的特征”為標準進行選定的。這里的長時間執行的最明顯特征是指令列復用,例如方法調用、循環跳轉、異常跳轉等。
虛擬機為了能讓所有線程都“跑”到安全點上停頓下來,設計了兩個方案:搶先式中斷和主動式中斷。其中搶先式中斷是虛擬機發生GC時,首先把所有線程全部中斷,如果發生有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。這種方式現在比較用了。而主動式中斷是虛擬機需要GC時僅僅簡單的設置一個標志,各個線程執行到安全點時主動去輪詢這個標志,發現中斷標志為真時就自己中斷掛起。
三、如何回收?
3.1 垃圾收集算法:
(1)標記-清除(Mark-Sweep)算法
這是最基礎的算法,就像它名字一樣,算法分為“標記”和“清除”兩個階段:首先標記處所有需要回收的對象(如哪些內存需要回收所描述的對象),對標記完成后統一回收所有被標記的對象,如下圖所示:
缺點:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除后悔產生大量的不連續的內存碎片,可能會導致后續無法分配大對象而導致再一次觸發垃圾收集動作。
(2)復制算法
為了針對標記-清除算法的不足,復制算法將可用內存容量劃分為大小相等的兩塊,每次只使用一塊。當一塊的內存用完了,就將還存活的對象復制到另一塊上面去。然后把已使用過的內存空間一次清理掉,如下圖所示:
缺點:使用內存比原來縮小了一半。
現在的商業虛擬機都采用這種收集算法來回收新生代,有企業分析的得出其實并不需求將內存按1:1的比例劃分,因為新生代中的對象大部分都是“朝生夕死”的。所以,HotSpot虛擬機默認的Eden和Survivor的大小比例是8:1。一塊Eden和兩塊Survivor,每次使用一塊Eden和一塊Survivor,也就是說只有10%是浪費的。如果另一塊Survivor都無法存放上次垃圾回收的對象時,那這些對象將通過“擔保機制”進入老年代了。
(3)標記-整理(Mark-Compact)算法
復制算法一般是對對象存活率較低的一種回收操作,但對于對象存活率較高的內存區域(老年代)來說,效果就不是那么理想了,標記-整理算法因此誕生了。標記-整理算法和標記-清除算法差不多,都是一開始對回收對象進行標記,但后續不是直接對對象清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存,如下圖所示:
(4)分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據對象存活的生命周期將內存劃分為若干個不同的區域。一般情況下將堆區劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那么就可以根據不同代的特點采取最適合的收集算法。
3.2 垃圾收集器:
(1)七種垃圾收集器:
說明:
注意:并行與并發
(2)常用五種組合:
(2.1)Serial/Serial Old:
特點:
說明:
STW(stop the world):編譯代碼時為每一個方法注入safepoint(方法中循環結束的點、方法執行結束的點),在暫停應用時,需要等待所有的用戶線程進入safepoint,之后暫停所有線程,然后進行垃圾回收。
適用場合:
(2.2)ParNew/Serial Old:
說明:
ParNew除了采用多GC線程來實現復制算法以外,其他都與Serial一樣,但是此組合中的Serial Old又是一個單GC線程,所以該組合是一個比較尷尬的組合,在單CPU情況下沒有Serial/Serial Old速度快(因為ParNew多線程需要切換),在多CPU情況下又沒有之后的三種組合快(因為Serial Old是單GC線程),所以使用其實不多。
-XX:ParallelGCThreads:指定ParNew GC線程的數量,默認與CPU核數相同,該參數在于CMS GC組合時,也可能會用到
(2.3)Parallel Scavenge/Parallel Old:
特點:
說明:
參數設置:
適用場合:
(2.4)ParNew/CMS:
說明:
特點:
1.年輕代ParNew收集器采用多個GC線程實現"復制"算法(包括掃描、復制)
2.年老代CMS收集器采用多線程實現"標記-清除"算法
3.初始標記與重新標記都會暫停所有用戶線程(即STW),但是時間較短;并發標記與并發清理時間較長,但是不需要STW
關于并發標記期間怎樣記錄發生變動的引用關系對象,在重新標記期間怎樣掃描這些對象
缺點:
參數設置:
適用場合:
用于處理很多的交互任務的情況
方法區的回收一般使用CMS,配置兩個參數:-XX:+CMSPermGenSweepingEnabled與-XX:+CMSClassUnloadingEnabled
適用于一些需要長期運行且對相應時間有一定要求的后臺程序
(2.5)G1
說明:
原理:
運作流程:
優點:
適用范圍:
關于深入淺析JVM中的垃圾收集器就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。