JVM垃圾回收

JVM垃圾回收

Java堆中存放著大量的Java對(duì)象實(shí)例,在垃圾收集器回收內(nèi)存前,第一件事情就是確定哪些對(duì)象是“活著的”,哪些是可以回收的。

引用計(jì)數(shù)算法

引用計(jì)數(shù)算法是判斷對(duì)象是否存活的基本算法:給每個(gè)對(duì)象添加一個(gè)引用計(jì)數(shù)器,沒當(dāng)一個(gè)地方引用它的時(shí)候,計(jì)數(shù)器值加1;當(dāng)引用失效后,計(jì)數(shù)器值減1。但是這種方法有一個(gè)致命的缺陷,當(dāng)兩個(gè)對(duì)象相互引用時(shí)會(huì)導(dǎo)致這兩個(gè)都無法被回收。

根搜索算法

在主流的商用語言中(Java、C#...)都是使用根搜索算法來判斷對(duì)象是否存活。對(duì)于程序來說,根對(duì)象總是可以訪問的。從這些根對(duì)象開始,任何可以被觸及的對(duì)象都被認(rèn)為是"活著的"的對(duì)象。無法觸及的對(duì)象被認(rèn)為是垃圾,需要被回收。

Java虛擬機(jī)的根對(duì)象集合根據(jù)實(shí)現(xiàn)不同而不同,但是總會(huì)包含以下幾個(gè)方面:

  • 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。
  • 方法區(qū)中的類靜態(tài)屬性引用的變量。
  • 方法區(qū)中的常量引用的變量。
  • 本地方法JNI的引用對(duì)象。

區(qū)分活動(dòng)對(duì)象和垃圾的兩個(gè)基本方法是引用計(jì)數(shù)和根搜索。 引用計(jì)數(shù)是通過為堆中每個(gè)對(duì)象保存一個(gè)計(jì)數(shù)來區(qū)分活動(dòng)對(duì)象和垃圾。根搜索算法實(shí)際上是追蹤從根結(jié)點(diǎn)開始的引用圖。

引用對(duì)象

引用對(duì)象封裝了指向其他對(duì)象的連接:被指向的對(duì)象稱為引用目標(biāo)。Reference有三個(gè)直接子類SoftReference、WeakReference、PhantomReference分別代表:軟引用、弱引用、虛引用。強(qiáng)引用在Java中是普遍存在的,類似Object o = new Object();這類引用就是強(qiáng)引用,強(qiáng)引用和以上引用的區(qū)別在于:強(qiáng)引用禁止引用目標(biāo)被垃圾收集器收集,而其他引用不禁止。

當(dāng)使用軟引用、弱引用、虛引用時(shí),并且對(duì)可觸及性狀態(tài)的改變有興趣,可以把引用對(duì)象和引用隊(duì)列關(guān)聯(lián)起來。

對(duì)象有六種可觸及狀態(tài)變化:

  • 強(qiáng)可觸及:對(duì)象可以從根節(jié)點(diǎn)不通過任何引用對(duì)象搜索到。垃圾收集器不會(huì)回收這個(gè)對(duì)象的內(nèi)存空間。

  • 軟可觸及:對(duì)象可以從根節(jié)點(diǎn)通過一個(gè)或多個(gè)(未被清除的)軟引用對(duì)象觸及,垃圾收集器在要發(fā)生內(nèi)存溢出前將這些對(duì)象列入回收范圍中進(jìn)行回收,如果該軟引用對(duì)象和引用隊(duì)列相關(guān)聯(lián),它會(huì)把該軟引用對(duì)象加入隊(duì)列。

SoftReference可以用來創(chuàng)建內(nèi)存中緩存,JVM的實(shí)現(xiàn)需要在拋出OutOfMemoryError之前清除軟引用,但在其他的情況下可以選擇清理的時(shí)間或者是否清除它們。

  • 弱可觸及:對(duì)象可以從根節(jié)點(diǎn)開始通過一個(gè)或多個(gè)(未被清除的)弱引用對(duì)象觸及,垃圾收集器在一次GC的時(shí)候會(huì)回收所有的弱引用對(duì)象,如果該弱引用對(duì)象和引用隊(duì)列相關(guān)聯(lián),它會(huì)把該弱引用對(duì)象加入隊(duì)列。

  • 可復(fù)活的:對(duì)象既不是強(qiáng)可觸及、軟可觸及、也不是弱可觸及,但仍然可能通過執(zhí)行某些終結(jié)方法復(fù)活到這幾個(gè)狀態(tài)之一。

Java類可以通過重寫finalize方法復(fù)活準(zhǔn)備回收的對(duì)象,但finalize方法只是在對(duì)象第一次回收時(shí)會(huì)調(diào)用。

  • 虛可觸及垃圾收集器不會(huì)清除一個(gè)虛引用,所有的虛引用都必須由程序明確的清除。 同時(shí)也不能通過虛引用來取得一個(gè)對(duì)象的實(shí)例。

  • 不可觸及:不可觸及對(duì)象已經(jīng)準(zhǔn)備好回收了。

若一個(gè)對(duì)象的引用類型有多個(gè),那到底如何判斷它的可達(dá)性呢?其實(shí)規(guī)則如下:

  1. 單條引用鏈的可達(dá)性以最弱的一個(gè)引用類型來決定;
  2. 多條引用鏈的可達(dá)性以最強(qiáng)的一個(gè)引用類型來決定;

垃圾回收算法

標(biāo)記--清除算法

首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象,標(biāo)記的方法使用根搜索算法。主要有兩個(gè)缺點(diǎn):

  • 效率問題,標(biāo)記和清除的效率都不高。

  • 空間問題,標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片。

復(fù)制回收算法

將可用內(nèi)存分為大小相等的兩份,在同一時(shí)刻只使用其中的一份。當(dāng)這一份內(nèi)存使用完了,就將還存活的對(duì)象復(fù)制到另一份上,然后將這一份上的內(nèi)存清空。復(fù)制算法能有效避免內(nèi)存碎片,但是算法需要將內(nèi)存一分為二,導(dǎo)致內(nèi)存使用率大大降低。

標(biāo)記--整理算法

復(fù)制算法在對(duì)象存活率較高的情況下會(huì)復(fù)制很多的對(duì)象,效率會(huì)很低。標(biāo)記--整理算法就解決了這樣的問題,標(biāo)記過程和標(biāo)記--清除算法一樣,但后續(xù)是將所有存活的對(duì)象都移動(dòng)到內(nèi)存的一端,然后清理掉端外界的對(duì)象。

分代回收算法

在JVM中不同的對(duì)象擁有不同的生命周期,因此對(duì)于不同生命周期的對(duì)象也可以采用不同的垃圾回收方法,以提高效率,這就是分代回收算法的核心思想。

在不進(jìn)行對(duì)象存活時(shí)間區(qū)分的情況下,每次垃圾回收都是對(duì)整個(gè)堆空間進(jìn)行回收,花費(fèi)的時(shí)間相對(duì)會(huì)長(zhǎng)。同時(shí),因?yàn)槊看位厥斩夹枰闅v所有存活對(duì)象,但實(shí)際上,對(duì)于生命周期長(zhǎng)的對(duì)象而言,這種遍歷是沒有效果的,因?yàn)榭赡苓M(jìn)行了很多次遍歷,但是他們依舊存在。因此,分代垃圾回收采用分治的思想,進(jìn)行代的劃分,把不同生命周期的對(duì)象放在不同代上,不同代上采用最適合它的垃圾回收方式進(jìn)行回收。

JVM中的共劃分為三個(gè)代:新生代(Young Generation)、老年代(Old Generation)持久代(Permanent Generation)。其中持久代主要存放的是Java類的類信息,與垃圾收集要收集的Java對(duì)象關(guān)系不大。

jvm-gc-1.gif
  • 新生代:所有新生成的對(duì)象首先都是放在新生代的,新生代采用復(fù)制回收算法。新生代的目標(biāo)就是盡可能快速的收集掉那些生命周期短的對(duì)象。新生代分三個(gè)區(qū)。一個(gè)Eden區(qū),兩個(gè)Survivor區(qū)(一般而言)。大部分對(duì)象在Eden區(qū)中生成。當(dāng)Eden區(qū)滿時(shí),還存活的對(duì)象將被復(fù)制到Survivor區(qū)(兩個(gè)中的一個(gè)),當(dāng)這個(gè)Survivor區(qū)滿時(shí),此區(qū)的存活對(duì)象將被復(fù)制到另外一個(gè)Survivor區(qū),當(dāng)這個(gè)Survivor去也滿了的時(shí)候,從第一個(gè)Survivor區(qū)復(fù)制過來的并且此時(shí)還存活的對(duì)象,將被復(fù)制“年老區(qū)(Tenured)”。需要注意,Survivor的兩個(gè)區(qū)是對(duì)稱的,沒先后關(guān)系,所以同一個(gè)區(qū)中可能同時(shí)存在從Eden復(fù)制過來 對(duì)象,和從前一個(gè)Survivor復(fù)制過來的對(duì)象,而復(fù)制到年老區(qū)的只有從第一個(gè)Survivor去過來的對(duì)象。而且,Survivor區(qū)總有一個(gè)是空的。

在HotSpot虛擬機(jī)內(nèi)部默認(rèn)Eden和Survivor的大小比例是8:1, 也就是每次新生代中可用內(nèi)存為整個(gè)新生代的90%,這大大提高了復(fù)制回收算法的效率。

  • 老年代:在新生代中經(jīng)歷了N次垃圾回收后仍然存活的對(duì)象,就會(huì)被放到老年代中,老年代采用標(biāo)記整理回收算法。因此,可以認(rèn)為老年代中存放的都是一些生命周期較長(zhǎng)的對(duì)象。

  • 持久代:用于存放靜態(tài)文件,如final常量,static常量,常量池等。持久代對(duì)垃圾回收沒有顯著影響,但是有些應(yīng)用可能動(dòng)態(tài)生成或者調(diào)用一些class,例如Hibernate等,在這種時(shí)候需要設(shè)置一個(gè)比較大的持久代空間來存放這些運(yùn)行過程中新增的類。

垃圾回收觸發(fā)條件

由于對(duì)象進(jìn)行了分代處理,因此垃圾回收區(qū)域、時(shí)間也不一樣。GC有兩種類型:Scavenge GC和Full GC。對(duì)于一個(gè)擁有終結(jié)方法的對(duì)象,在垃圾收集器釋放對(duì)象前必須執(zhí)行終結(jié)方法。但是當(dāng)垃圾收集器第二次收集這個(gè)對(duì)象時(shí)便不會(huì)再次調(diào)用終結(jié)方法。

Scavenge GC

一般情況下,當(dāng)新對(duì)象生成,并且在Eden申請(qǐng)空間失敗時(shí),就會(huì)觸發(fā)Scavenge GC,對(duì)Eden區(qū)域進(jìn)行GC,清除非存活對(duì)象,并且把尚且存活的對(duì)象移動(dòng)到Survivor區(qū),然后整理Survivor的兩個(gè)區(qū)。這種方式的GC是對(duì)新生代的Eden區(qū)進(jìn)行,不會(huì)影響到老年代。因?yàn)榇蟛糠謱?duì)象都是從Eden區(qū)開始的,同時(shí)Eden區(qū)不會(huì)分配的很大,所以Eden區(qū)的GC會(huì)頻繁進(jìn)行。

Full GC

對(duì)整個(gè)堆進(jìn)行整理,包括Young、Tenured和Perm。Full GC因?yàn)樾枰獙?duì)整個(gè)對(duì)進(jìn)行回收,所以比Scavenge GC要慢,因此應(yīng)該盡可能減少Full GC的次數(shù)。在對(duì)JVM調(diào)優(yōu)的過程中,很大一部分工作就是對(duì)于FullGC的調(diào)節(jié)。有如下原因可能導(dǎo)致Full GC:

  • 老年代(Tenured)被寫滿

  • 持久代(Perm)被寫滿

  • System.gc()被顯示調(diào)用

垃圾收集器

垃圾收集器是內(nèi)存回收的具體實(shí)現(xiàn),下圖展示了7種用于不同分代的收集器,兩個(gè)收集器之間有連線表示可以搭配使用。下面的這些收集器沒有“最好的”這一說,每種收集器都有最適合的使用場(chǎng)景。

Serial收集器

Serial收集器是最基本的收集器,這是一個(gè)單線程收集器,它“單線程”的意義不僅僅是說明它只用一個(gè)線程去完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集工作時(shí),必須暫停其他工作線程,直到它收集完成。Sun將這件事稱之為”Stop the world“。

沒有一個(gè)收集器能完全不停頓,只是停頓的時(shí)間長(zhǎng)短。

雖然Serial收集器的缺點(diǎn)很明顯,但是它仍然是JVM在Client模式下的默認(rèn)新生代收集器。它有著優(yōu)于其他收集器的地方:簡(jiǎn)單而高效(與其他收集器的單線程比較),Serial收集器由于沒有線程交互的開銷,專心只做垃圾收集自然也獲得最高的效率。在用戶桌面場(chǎng)景下,分配給JVM的內(nèi)存不會(huì)太多,停頓時(shí)間完全可以在幾十到一百多毫秒之間,只要收集不頻繁,這是完全可以接受的。

ParNew收集器

ParNew是Serial的多線程版本,在回收算法、對(duì)象分配原則上都是一致的。ParNew收集器是許多運(yùn)行在Server模式下的默認(rèn)新生代垃圾收集器,其主要在于除了Serial收集器,目前只有ParNew收集器能夠與CMS收集器配合工作。

Parallel Scavenge收集器

Parallel Scavenge收集器是一個(gè)新生代垃圾收集器,其使用的算法是復(fù)制算法,也是并行的多線程收集器。

Parallel Scavenge 收集器更關(guān)注可控制的吞吐量,吞吐量等于運(yùn)行用戶代碼的時(shí)間/(運(yùn)行用戶代碼的時(shí)間+垃圾收集時(shí)間)。直觀上,只要最大的垃圾收集停頓時(shí)間越小,吞吐量是越高的,但是GC停頓時(shí)間的縮短是以犧牲吞吐量和新生代空間作為代價(jià)的。比如原來10秒收集一次,每次停頓100毫秒,現(xiàn)在變成5秒收集一次,每次停頓70毫秒。停頓時(shí)間下降的同時(shí),吞吐量也下降了。

停頓時(shí)間越短就越適合需要與用戶交互的程序;而高吞吐量則可以最高效的利用CPU的時(shí)間,盡快的完成計(jì)算任務(wù),主要適用于后臺(tái)運(yùn)算。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是一個(gè)單線程收集器,采用“標(biāo)記-整理算法”進(jìn)行回收。其運(yùn)行過程與Serial收集器一樣。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和標(biāo)記-整理算法進(jìn)行垃圾回收。其通常與Parallel Scavenge收集器配合使用,“吞吐量?jī)?yōu)先”收集器是這個(gè)組合的特點(diǎn),在注重吞吐量和CPU資源敏感的場(chǎng)合,都可以使用這個(gè)組合。

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短停頓時(shí)間為目標(biāo)的收集器,CMS收集器采用標(biāo)記--清除算法,運(yùn)行在老年代。主要包含以下幾個(gè)步驟:

  • 初始標(biāo)記
  • 并發(fā)標(biāo)記
  • 重新標(biāo)記
  • 并發(fā)清除

其中初始標(biāo)記和重新標(biāo)記仍然需要“Stop the world”。初始標(biāo)記僅僅標(biāo)記GC Root能直接關(guān)聯(lián)的對(duì)象,并發(fā)標(biāo)記就是進(jìn)行GC Root Tracing過程,而重新標(biāo)記則是為了修正并發(fā)標(biāo)記期間,因用戶程序繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記變動(dòng)的那部分對(duì)象的標(biāo)記記錄。

由于整個(gè)過程中最耗時(shí)的并發(fā)標(biāo)記和并發(fā)清除,收集線程和用戶線程一起工作,所以總體上來說,CMS收集器回收過程是與用戶線程并發(fā)執(zhí)行的。雖然CMS優(yōu)點(diǎn)是并發(fā)收集、低停頓,很大程度上已經(jīng)是一個(gè)不錯(cuò)的垃圾收集器,但是還是有三個(gè)顯著的缺點(diǎn):

  • CMS收集器對(duì)CPU資源很敏感。在并發(fā)階段,雖然它不會(huì)導(dǎo)致用戶線程停頓,但是會(huì)因?yàn)檎加靡徊糠志€程(CPU資源)而導(dǎo)致應(yīng)用程序變慢。

  • CMS收集器不能處理浮動(dòng)垃圾。所謂的“浮動(dòng)垃圾”,就是在并發(fā)標(biāo)記階段,由于用戶程序在運(yùn)行,那么自然就會(huì)有新的垃圾產(chǎn)生,這部分垃圾被標(biāo)記過后,CMS無法在當(dāng)次集中處理它們,只好在下一次GC的時(shí)候處理,這部分未處理的垃圾就稱為“浮動(dòng)垃圾”。也是由于在垃圾收集階段程序還需要運(yùn)行,即還需要預(yù)留足夠的內(nèi)存空間供用戶使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎填滿才進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時(shí)程序運(yùn)作使用。要是CMS預(yù)留的內(nèi)存空間不能滿足程序的要求,這是JVM就會(huì)啟動(dòng)預(yù)備方案:臨時(shí)啟動(dòng)Serial Old收集器來收集老年代,這樣停頓的時(shí)間就會(huì)很長(zhǎng)。

  • 由于CMS使用標(biāo)記--清除算法,所以在收集之后會(huì)產(chǎn)生大量?jī)?nèi)存碎片。當(dāng)內(nèi)存碎片過多時(shí),將會(huì)給分配大對(duì)象帶來困難,這是就會(huì)進(jìn)行Full GC。

G1收集器

G1收集器與CMS相比有很大的改進(jìn):

  • G1收集器采用標(biāo)記--整理算法實(shí)現(xiàn)。
  • 可以非常精確地控制停頓。

G1收集器可以實(shí)現(xiàn)在基本不犧牲吞吐量的情況下完成低停頓的內(nèi)存回收,這是由于它極力的避免全區(qū)域的回收,G1收集器將Java堆(包括新生代和老年代)劃分為多個(gè)區(qū)域(Region),并在后臺(tái)維護(hù)一個(gè)優(yōu)先列表,每次根據(jù)允許的時(shí)間,優(yōu)先回收垃圾最多的區(qū)域 。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 0. 前言 JVM筆記系列,以JDK1.7為基準(zhǔn),主要以《深入理解Java虛擬機(jī)》(第二版)和《Java虛擬機(jī)規(guī)范...
    郭尋撫閱讀 1,037評(píng)論 0 3
  • 注意 : 本系列文章為學(xué)習(xí)系列,部分內(nèi)容會(huì)取自相關(guān)書籍或者網(wǎng)絡(luò)資源,在文章中間和末尾處會(huì)有標(biāo)注 垃圾回收的意義 它...
    lyk2112閱讀 367評(píng)論 0 0
  • 許多高級(jí)編程語言都帶有自動(dòng)垃圾回收特性,以將程序員從繁瑣復(fù)雜的內(nèi)存分配和釋放工作中解脫。本文將概述常見的垃圾回收算...
    kelgon閱讀 2,118評(píng)論 2 52
  • 常用的GC算法 1.引用計(jì)數(shù)法 給一個(gè)對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器的值就加1,當(dāng)引用失效...
    eliter0609閱讀 599評(píng)論 0 3
  • swift 采用Unicode編碼,幾乎涵蓋了所有我們知道的字符。 let andSign:Charaterar ...
    ClementGu閱讀 2,685評(píng)論 0 0

友情鏈接更多精彩內(nèi)容