您好,登錄后才能下訂單哦!
本篇文章為大家展示了如何進行JVM垃圾回收,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
首先要在這里介紹一下80/20 法則
:
>約僅有20%的變因操縱著80%的局面。也就是說:所有變量中,最重要的僅有20%,雖然剩余的80%占了多數(shù),控制的范圍卻遠低于“關(guān)鍵的少數(shù)”。
Java 對象的生命周期也滿足也這樣的定律,即大部分的 Java 對象只存活一小段時間,而存活下來的小部分 Java 對象則會存活很長一段時間。
因此,這也就造就了 JVM 中分代回收
的思想。簡單來說,就是將堆空間劃分為兩代,分別叫做新生代
和老年代
。新生代用來存儲新建的對象。當(dāng)對象存活時間夠長時,則將其移動到老年代。
這樣也就可以讓 JVM 給不同代使用不同的回收算法。
對于新生代,我們猜測大部分的 Java 對象只存活一小段時間,那么便可以頻繁地采用耗時較短的垃圾回收算法,讓大部分的垃圾都能夠在新生代被回收掉。
對于老年代,我們猜測大部分的垃圾已經(jīng)在新生代中被回收了,而在老年代中的對象有大概率會繼續(xù)存活。當(dāng)真正觸發(fā)針對老年代的回收時,則代表這個假設(shè)出錯了,或者堆的空間已經(jīng)耗盡了。此時,JVM 往往需要做一次全堆掃描,耗時也將不計成本。(當(dāng)然,現(xiàn)代的垃圾回收器都在并發(fā)收集的道路上發(fā)展,來避免這種全堆掃描的情況。)
那么,我們先來看看 JVM 中堆究竟是如何劃分的。
按照上文所述,JVM 將堆劃分為新生代和老年代,其中,新生代又被劃分為 Eden 區(qū),以及兩個大小相同的 Survivor 區(qū)。
通常來說,當(dāng)我們調(diào)用 new 指令時,它會在 Eden 區(qū)中劃出一塊作為存儲對象的內(nèi)存。由于堆空間是線程共享的,因此直接在這里邊劃空間是需要進行同步的。否則,將有可能出現(xiàn)兩個對象共用一段內(nèi)存的事故。
JVM 的解決方法是為每個線程預(yù)先申請一段連續(xù)的堆空間,并且只允許每個線程在自己申請過的堆空間中創(chuàng)建對象,如果申請的堆空間被用完了,那么再繼續(xù)申請即可,這也就是 TLAB(Thread Local Allocation Buffer,對應(yīng)虛擬機參數(shù) -XX:+UseTLAB,默認開啟)。
此時,如果線程操作涉及到加鎖,則該線程需要維護兩個指針(實際上可能更多,但重要也就兩個),一個指向 TLAB 中空余內(nèi)存的起始位置,一個則指向 TLAB 末尾。
接下來的 new 指令,便可以直接通過指針加法(bump the pointer)來實現(xiàn),即把指向空余內(nèi)存位置的指針加上所請求的字節(jié)數(shù)。
如果加法后空余內(nèi)存指針的值仍小于或等于指向末尾的指針,則代表分配成功。否則,TLAB 已經(jīng)沒有足夠的空間來滿足本次新建操作。這個時候,便需要當(dāng)前線程重新申請新的 TLAB。
那有沒有可能出現(xiàn)申請不到的情況呢?有的,這個時候就會觸發(fā)Minor GC
了。
所謂 Minor GC,就是指: >當(dāng) Eden 區(qū)的空間耗盡時,JVM 會進行一次 Minor GC,來收集新生代的垃圾。存活下來的對象,則會被送到 Survivor 區(qū)。
上文提到,新生代共有兩個 Survivor 區(qū),我們分別用 from 和 to 來指代。其中 to 指向的 Survivior 區(qū)是空的。
當(dāng)發(fā)生 Minor GC 時,Eden 區(qū)和 from 指向的 Survivor 區(qū)中的存活對象會被復(fù)制到 to 指向的 Survivor 區(qū)中,然后交換 from 和 to 指針,以保證下一次 Minor GC 時,to 指向的 Survivor 區(qū)還是空的。
JVM 會記錄 Survivor 區(qū)中每個對象一共被來回復(fù)制了幾次。如果一個對象被復(fù)制的次數(shù)為 15(對應(yīng)虛擬機參數(shù) -XX:+MaxTenuringThreshold),那么該對象將被晉升(promote)至老年代。
另外,如果單個 Survivor 區(qū)已經(jīng)被占用了 50%(對應(yīng)虛擬機參數(shù) -XX:TargetSurvivorRatio),那么較高復(fù)制次數(shù)的對象也會被晉升至老年代。
總而言之,當(dāng)發(fā)生 Minor GC 時,我們應(yīng)用了標記 - 復(fù)制
算法,將 Survivor 區(qū)中的老存活對象晉升到老年代,然后將剩下的存活對象和 Eden 區(qū)的存活對象復(fù)制到另一個 Survivor 區(qū)中。理想情況下,Eden 區(qū)中的對象基本都死亡了,那么需要復(fù)制的數(shù)據(jù)將非常少,因此采用這種標記 - 復(fù)制
算法的效果極好。
Minor GC 的另外一個好處是不用對整個堆進行垃圾回收。但是,它卻有一個問題,那就是老年代中的對象可能引用新生代的對象。也就是說,在標記存活對象的時候,我們需要掃描老年代中的對象。如果該對象擁有對新生代對象的引用,那么這個引用也會被作為 GC Roots。這樣一來,豈不是又做了一次全堆掃描呢?
為了避免掃描全堆,JVM 引入了名為卡表
的技術(shù),大致地標出可能存在老年代到新生代引用的內(nèi)存區(qū)域。有興趣的朋友可以去詳細了解一下,這里限于篇幅,就不具體介紹了。
那什么時候會發(fā)生Full GC
呢?針對不同的垃圾收集器,F(xiàn)ull GC 的觸發(fā)條件可能不都一樣。按 HotSpot VM 的 serial GC 的實現(xiàn)來看,觸發(fā)條件是:
>當(dāng)準備要觸發(fā)一次 Minor GC 時,如果發(fā)現(xiàn)統(tǒng)計數(shù)據(jù)說之前 Minor GC 的平均晉升大小比目前老年代剩余的空間大,則不會觸發(fā) Minor GC 而是轉(zhuǎn)為觸發(fā) Full GC。 > >因為 HotSpot VM 的 GC 里,除了垃圾回收器 CMS 能單獨收集老年代之外,其他的 GC 都會同時收集整個堆,所以不需要事先準備一次單獨的 Minor GC。
基礎(chǔ)的回收方式有三種:清除
、壓縮
、復(fù)制
,接下來讓我們來一一了解一下。
所謂清除,就是把死亡對象所占據(jù)的內(nèi)存標記為空閑內(nèi)存,并記錄在一個空閑列表之中。當(dāng)需要新建對象時,內(nèi)存管理模塊便會從該空閑列表中尋找空閑內(nèi)存,并劃分給新建的對象。
其原理十分簡單,但是有兩個缺點:
會造成內(nèi)存碎片。由于 JVM 的堆中對象必須是連續(xù)分布的,因此可能出現(xiàn)總空閑內(nèi)存足夠,但是無法分配的極端情況。
分配效率較低。如果是一塊連續(xù)的內(nèi)存空間,那么我們可以通過指針加法(pointer bumping)來做分配。而對于空閑列表,JVM 則需要逐個訪問空閑列表中的項,來查找能夠放入新建對象的空閑內(nèi)存。
所謂壓縮,就是把存活的對象聚集到內(nèi)存區(qū)域的起始位置,從而留下一段連續(xù)的內(nèi)存空間。
這種做法能夠解決內(nèi)存碎片化的問題,但代價是壓縮算法的性能開銷,因此分配效率問題依舊沒有解決。
所謂復(fù)制,就是把內(nèi)存區(qū)域平均分為兩塊,分別用兩個指針 from 和 to 來維護,并且只是用 from 指針指向的內(nèi)存區(qū)域來分配內(nèi)存。當(dāng)發(fā)生垃圾回收時,便把存活的對象復(fù)制到 to 指針所指向的內(nèi)存區(qū)域中,并且交換 from 指針和 to 指針的內(nèi)容。
這種回收方式同樣能夠解決內(nèi)存碎片化的問題,但是它的缺點也極其明顯,即堆空間的使用效率極其低下。
針對新生代
的垃圾回收器共有三個:Serial ,Parallel Scavenge 和 Parallel New。這三個采用的都是標記 - 復(fù)制
算法。
其中,Serial 是一個單線程的,Parallel New 可以看成是 Serial 的多線程版本,Parallel Scavenge 和 Parallel New 類似,但更加注重吞吐率。此外,Parallel Scavenge 不能與 CMS 一起使用。
針對老年代
的垃圾回收器也有三個:Serial Old ,Parallel Old 和 CMS。
Serial Old 和 Parallel Old 都是標記 - 壓縮
算法。同樣,前者是單線程的,而后者可以看成前者的多線程版本。
CMS 采用的是標記 - 清除
算法,并且是并發(fā)的。除了少數(shù)幾個操作需要 STW(Stop the world) 之外,它可以在應(yīng)用程序運行過程中進行垃圾回收。在并發(fā)收集失敗的情況下,JVM 會使用其他兩個壓縮型垃圾回收器進行一次垃圾回收。由于 G1 的出現(xiàn),CMS 在 Java 9 中已被廢棄。
G1(Garbage First)是一個橫跨新生代和老年代的垃圾回收器。實際上,它已經(jīng)打亂了前面所說的堆結(jié)構(gòu),直接將堆分成極其多個區(qū)域。每個區(qū)域都可以充當(dāng) Eden 區(qū)、Survivor 區(qū)或者老年代中的一個。它采用的是標記 - 壓縮
算法,而且和 CMS 一樣都能夠在應(yīng)用程序運行過程中并發(fā)地進行垃圾回收。
G1 能夠針對每個細分的區(qū)域來進行垃圾回收。在選擇進行垃圾回收的區(qū)域時,它會優(yōu)先回收死亡對象較多的區(qū)域。這也是 G1 名字的由來。
主要講述的是 JVM 中具體的垃圾回收方法,從對象的生存規(guī)律,引出回收方法,結(jié)合多線程的特點,逐步優(yōu)化,最終產(chǎn)生了我們現(xiàn)在所能知道各種垃圾收集器。
上述內(nèi)容就是如何進行JVM垃圾回收,你們學(xué)到知識或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識儲備,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。