接着上一篇,介紹完了 JVM 中識別須要回收的垃圾對象以後,這一篇咱們來講說 JVM 是如何進行垃圾回收。 git
首先要在這裏介紹一下80/20 法則
:github
約僅有20%的變因操縱着80%的局面。也就是說:全部變量中,最重要的僅有20%,雖然剩餘的80%佔了多數,控制的範圍卻遠低於「關鍵的少數」。算法
Java 對象的生命週期也知足也這樣的定律,即大部分的 Java 對象只存活一小段時間,而存活下來的小部分 Java 對象則會存活很長一段時間。多線程
所以,這也就造就了 JVM 中分代回收
的思想。簡單來講,就是將堆空間劃分爲兩代,分別叫作新生代
和老年代
。新生代用來存儲新建的對象。當對象存活時間夠長時,則將其移動到老年代。併發
這樣也就可讓 JVM 給不一樣代使用不一樣的回收算法。性能
對於新生代,咱們猜想大部分的 Java 對象只存活一小段時間,那麼即可以頻繁地採用耗時較短的垃圾回收算法,讓大部分的垃圾都可以在新生代被回收掉。優化
對於老年代,咱們猜想大部分的垃圾已經在新生代中被回收了,而在老年代中的對象有大機率會繼續存活。當真正觸發針對老年代的回收時,則表明這個假設出錯了,或者堆的空間已經耗盡了。此時,JVM 每每須要作一次全堆掃描,耗時也將不計成本。(固然,現代的垃圾回收器都在併發收集的道路上發展,來避免這種全堆掃描的狀況。)線程
那麼,咱們先來看看 JVM 中堆到底是如何劃分的。指針
按照上文所述,JVM 將堆劃分爲新生代和老年代,其中,新生代又被劃分爲 Eden 區,以及兩個大小相同的 Survivor 區。code
一般來講,當咱們調用 new 指令時,它會在 Eden 區中劃出一塊做爲存儲對象的內存。因爲堆空間是線程共享的,所以直接在這裏邊劃空間是須要進行同步的。不然,將有可能出現兩個對象共用一段內存的事故。
JVM 的解決方法是爲每一個線程預先申請一段連續的堆空間,而且只容許每一個線程在本身申請過的堆空間中建立對象,若是申請的堆空間被用完了,那麼再繼續申請便可,這也就是 TLAB(Thread Local Allocation Buffer,對應虛擬機參數 -XX:+UseTLAB,默認開啓)。
此時,若是線程操做涉及到加鎖,則該線程須要維護兩個指針(實際上可能更多,但重要也就兩個),一個指向 TLAB 中空餘內存的起始位置,一個則指向 TLAB 末尾。
接下來的 new 指令,即可以直接經過指針加法(bump the pointer)來實現,即把指向空餘內存位置的指針加上所請求的字節數。
若是加法後空餘內存指針的值仍小於或等於指向末尾的指針,則表明分配成功。不然,TLAB 已經沒有足夠的空間來知足本次新建操做。這個時候,便須要當前線程從新申請新的 TLAB。
那有沒有可能出現申請不到的狀況呢?有的,這個時候就會觸發Minor GC
了。
所謂 Minor GC,就是指:
當 Eden 區的空間耗盡時,JVM 會進行一次 Minor GC,來收集新生代的垃圾。存活下來的對象,則會被送到 Survivor 區。
上文提到,新生代共有兩個 Survivor 區,咱們分別用 from 和 to 來指代。其中 to 指向的 Survivior 區是空的。
當發生 Minor GC 時,Eden 區和 from 指向的 Survivor 區中的存活對象會被複制到 to 指向的 Survivor 區中,而後交換 from 和 to 指針,以保證下一次 Minor GC 時,to 指向的 Survivor 區仍是空的。
JVM 會記錄 Survivor 區中每一個對象一共被來回複製了幾回。若是一個對象被複制的次數爲 15(對應虛擬機參數 -XX:+MaxTenuringThreshold),那麼該對象將被晉升(promote)至老年代。
另外,若是單個 Survivor 區已經被佔用了 50%(對應虛擬機參數 -XX:TargetSurvivorRatio),那麼較高複製次數的對象也會被晉升至老年代。
總而言之,當發生 Minor GC 時,咱們應用了標記 - 複製
算法,將 Survivor 區中的老存活對象晉升到老年代,而後將剩下的存活對象和 Eden 區的存活對象複製到另外一個 Survivor 區中。理想狀況下,Eden 區中的對象基本都死亡了,那麼須要複製的數據將很是少,所以採用這種標記 - 複製
算法的效果極好。
Minor GC 的另一個好處是不用對整個堆進行垃圾回收。可是,它卻有一個問題,那就是老年代中的對象可能引用新生代的對象。也就是說,在標記存活對象的時候,咱們須要掃描老年代中的對象。若是該對象擁有對新生代對象的引用,那麼這個引用也會被做爲 GC Roots。這樣一來,豈不是又作了一次全堆掃描呢?
爲了不掃描全堆,JVM 引入了名爲卡表
的技術,大體地標出可能存在老年代到新生代引用的內存區域。有興趣的朋友能夠去詳細瞭解一下,這裏限於篇幅,就不具體介紹了。
那何時會發生Full GC
呢?針對不一樣的垃圾收集器,Full GC 的觸發條件可能不都同樣。按 HotSpot VM 的 serial GC 的實現來看,觸發條件是:
當準備要觸發一次 Minor GC 時,若是發現統計數聽說以前 Minor GC 的平均晉升大小比目前老年代剩餘的空間大,則不會觸發 Minor GC 而是轉爲觸發 Full GC。
由於 HotSpot VM 的 GC 裏,除了垃圾回收器 CMS 能單獨收集老年代以外,其餘的 GC 都會同時收集整個堆,因此不須要事先準備一次單獨的 Minor GC。
基礎的回收方式有三種:清除
、壓縮
、複製
,接下來讓咱們來一一瞭解一下。
所謂清除,就是把死亡對象所佔據的內存標記爲空閒內存,並記錄在一個空閒列表之中。當須要新建對象時,內存管理模塊便會從該空閒列表中尋找空閒內存,並劃分給新建的對象。
其原理十分簡單,可是有兩個缺點:
所謂壓縮,就是把存活的對象彙集到內存區域的起始位置,從而留下一段連續的內存空間。
這種作法可以解決內存碎片化的問題,但代價是壓縮算法的性能開銷,所以分配效率問題依舊沒有解決。
所謂複製,就是把內存區域平均分爲兩塊,分別用兩個指針 from 和 to 來維護,而且只是用 from 指針指向的內存區域來分配內存。當發生垃圾回收時,便把存活的對象複製到 to 指針所指向的內存區域中,而且交換 from 指針和 to 指針的內容。
這種回收方式一樣可以解決內存碎片化的問題,可是它的缺點也極其明顯,即堆空間的使用效率極其低下。
針對新生代
的垃圾回收器共有三個:Serial ,Parallel Scavenge 和 Parallel New。這三個採用的都是標記 - 複製
算法。
其中,Serial 是一個單線程的,Parallel New 能夠當作是 Serial 的多線程版本,Parallel Scavenge 和 Parallel New 相似,但更加註重吞吐率。此外,Parallel Scavenge 不能與 CMS 一塊兒使用。
針對老年代
的垃圾回收器也有三個:Serial Old ,Parallel Old 和 CMS。
Serial Old 和 Parallel Old 都是標記 - 壓縮
算法。一樣,前者是單線程的,然後者能夠當作前者的多線程版本。
CMS 採用的是標記 - 清除
算法,而且是併發的。除了少數幾個操做須要 STW(Stop the world) 以外,它能夠在應用程序運行過程當中進行垃圾回收。在併發收集失敗的狀況下,JVM 會使用其餘兩個壓縮型垃圾回收器進行一次垃圾回收。因爲 G1 的出現,CMS 在 Java 9 中已被廢棄。
G1(Garbage First)是一個橫跨新生代和老年代的垃圾回收器。實際上,它已經打亂了前面所說的堆結構,直接將堆分紅極其多個區域。每一個區域均可以充當 Eden 區、Survivor 區或者老年代中的一個。它採用的是標記 - 壓縮
算法,並且和 CMS 同樣都可以在應用程序運行過程當中併發地進行垃圾回收。
G1 可以針對每一個細分的區域來進行垃圾回收。在選擇進行垃圾回收的區域時,它會優先回收死亡對象較多的區域。這也是 G1 名字的由來。
這篇文章主要講述的是 JVM 中具體的垃圾回收方法,從對象的生存規律,引出回收方法,結合多線程的特色,逐步優化,最終產生了咱們如今所能知道各類垃圾收集器。
有興趣的話能夠訪問個人博客或者關注個人公衆號、頭條號,說不定會有意外的驚喜。