垃圾回收的常見算法

垃圾回收的常見算法

2.1 引用計數(shù)法

2.1.1 原理

2.1.2 優(yōu)缺點

2.2 標(biāo)記清除法

2.2.1 原理

2.2.2 優(yōu)缺點

2.3 標(biāo)記壓縮算法

2.3.1 原理

2.3.2 優(yōu)缺點

2.4 復(fù)制算法

2.4.1 JVM中年輕代內(nèi)存空間

2.4.2 優(yōu)缺點

2.5 分代算法

3 垃圾收集器以及內(nèi)存分配

3.1 串行垃圾收集器

3.1.1 編寫測試代碼

3.1.2 設(shè)置垃圾回收為串行收集器

3.2 并行垃圾收集器

3.2.1 ParNew垃圾收集器

3.2.2 ParallelGC垃圾收集器

3.3 CMS垃圾收集器

3.3.1 測試

3.4 G1垃圾收集器(重點)

3.4.1 原理

3.4.2 Young GC

3.4.2.1 Remembered Set(已記憶集合)

3.4.3 Mixed GC

3.4.3.1 全局并發(fā)標(biāo)記

3.4.3.2 拷貝存活對象

3.4.4 G1收集器相關(guān)參數(shù)

3.4.5 測試

3.4.6 對于G1垃圾收集器優(yōu)化建議

4 可視化GC日志分析工具

4.1 GC日志輸出參數(shù)

4.2 GC Easy可視化工具

自動化的管理內(nèi)存資源,垃圾回收機制必須要有一套算法來進(jìn)行計算,那些是有效的對象,那些是無效的對象,對于無效的對象

就要進(jìn)行回收處理。

常見的垃圾回收算法有 :引用計數(shù)法、標(biāo)記清除法、標(biāo)記壓縮法、復(fù)制算法、分代算法等。

2.1 引用計數(shù)法

引用計數(shù)是歷史最悠久的一種算法,最早George E. Collins在1960的首次提出,50年后的今天,該算法依然被很多編程語言使用。

2.1.1 原理

假設(shè)有一個對象A,任何一個對象對A的引用,那么對象A的引用計數(shù)器+1,當(dāng)引用失敗時,對象A的引用計數(shù)器就-1,如果對象A的計算器的值

為0,就說明對象A沒有引用了,可以被回收。如圖所示

2.1.2 優(yōu)缺點

優(yōu)點 :

1、實時性較高,無需等到內(nèi)存不夠的時候,才開始回收,運行時根據(jù)對象的計數(shù)器是否為0,就可以直接回收。

2、在垃圾回收過程中,應(yīng)用無需掛起。如果申請內(nèi)存時,內(nèi)存不足,則立刻報outofmember錯誤。

3、區(qū)域性,更新對象的計數(shù)器時,只是影響到該對象,不會掃描全部對象。

缺點 :

1、每次對象唄引用時,都需要去更新計數(shù)器,有一點時間開銷。

2、浪費CPU資源,即使內(nèi)存夠用,任然在運行時進(jìn)行計數(shù)器的統(tǒng)計。

3、無法解決循環(huán)引用問題。(最大的缺點)

雖然a和b都為null,但是由于a和b存在循環(huán)引用,這樣a和b永遠(yuǎn)都不回被回收。

2.2 標(biāo)記清除法

標(biāo)記清除算法,是將垃圾回收分為2個階段,分別是標(biāo)記和清除。

標(biāo)記 :從根節(jié)點開始標(biāo)記引用的對象。

清除 :未被標(biāo)記引用的對象就是垃圾對象,可以被清理。

2.2.1 原理

這張圖代表的是程序運行期間所有對象的狀態(tài),它們的標(biāo)志位全部是0(也就是未標(biāo)記,以下默認(rèn)0就是未標(biāo)記,1為已標(biāo)記),

假設(shè)這會兒有效內(nèi)存空間耗盡了,JVM將會停止應(yīng)用程序的運行并開啟GC線程,然后開始進(jìn)行標(biāo)記工作,按照根搜索算法,標(biāo)記完以后,對象的狀態(tài)如下圖。

可以看到,按照根搜索算法,所有從root對象可達(dá)的對象就被標(biāo)記為存活的對象,此時已經(jīng)完成了第一階段標(biāo)記。接下來,就要

執(zhí)行第二階段清除了,那么清除完以后,剩下的對象以及對象的狀態(tài)如下圖所示。

可以看到,沒有被標(biāo)記的對象將會回收清除掉,而被標(biāo)記的對象將會留下,并且會將標(biāo)記重新歸0.接下來就不用說了,喚醒停止

的程序線程,讓程序繼續(xù)運行即可。

2.2.2 優(yōu)缺點

可以看到,標(biāo)記清除算法解決了引用計數(shù)算法中的循環(huán)引用的問題,沒有從root節(jié)點引用的對象都會被回收。同樣,標(biāo)記清除算法也是有缺點的 :

1、效率較低,標(biāo)記和清除兩個動作都需要遍歷所有的對象,并且在GC時,需要停止應(yīng)用程序,對于交互性要求比較高的應(yīng)用

而言這個體驗是非常差的。

2、通過標(biāo)記清除算法清理出來的內(nèi)容,碎片化較為嚴(yán)重,因為被回收的對象可能存在于內(nèi)存的各個角落,所以清理出來的內(nèi)存是不連貫的。

2.3 標(biāo)記壓縮算法

標(biāo)記壓縮算法是在標(biāo)記清除算法的基礎(chǔ)之上,做了優(yōu)化改進(jìn)的算法。和標(biāo)記清除算法一樣,也是從根節(jié)點開始,對對象的引用進(jìn)行標(biāo)記,在清理階段,并不是簡單的清理未標(biāo)記的對象,而是將存活的對象壓縮到內(nèi)存的一端,然后清理邊界以外的垃圾,從而解決了碎片化的問題。

2.3.1 原理

2.3.2 優(yōu)缺點

優(yōu)缺點同標(biāo)記清除算法,解決了標(biāo)記清除算法的碎片化的問題,同時,標(biāo)記壓縮算法多了一步,對象移動內(nèi)存位置的步驟,其效率也有一定的影響。

2.4 復(fù)制算法

復(fù)制算法的核心就是,將原有的內(nèi)存空間一分為二,每次只用其中的一塊,在垃圾回收時,將正在使用的對象復(fù)制到另一個內(nèi)存空間中,然后將該內(nèi)存空間清空,交換兩個內(nèi)存的角色,完成垃圾的回收。

如果內(nèi)存中的垃圾對象較多,需要復(fù)制的對象就較少,這種情況下適合使用該方式并且效率比較高,反之,則不適合。

2.4.1 JVM中年輕代內(nèi)存空間

1、在GC開始的時候,對象只會存在于Eden區(qū)和名為“From”的Survivor區(qū),Survivor區(qū)“To”是空的。

2、緊接著進(jìn)行GC,Eden區(qū)中所有存活的對象都會被復(fù)制到“To”,而在“From”區(qū)中,仍存活的對象會根據(jù)它們的年齡值來決定去向。年齡達(dá)到一定值(年齡閥值,可以通過-XX:MaxTenuringThreshold來設(shè)置)的對象會被移動到年老代中,沒有達(dá)到閥值的對象會被復(fù)制到“To”區(qū)域。

3、經(jīng)過這次GC后,Eden區(qū)和From區(qū)已經(jīng)被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區(qū)域是空的。

4、GC會一直重復(fù)這樣的過程,直到“To”區(qū)被填滿,“To”區(qū)被填滿之后,會將所有對象移動到年老代中。

2.4.2 優(yōu)缺點

優(yōu)點 :

1、在垃圾對象多的情況下,效率較高。

2、清理后,內(nèi)存無碎片。

缺點 :

1、在垃圾對象少的情況下,不適用,如 :老年代內(nèi)存。

2、分配的2塊內(nèi)存空間,在同一時刻,只能使用一半,內(nèi)存使用率較低。

2.5 分代算法

前面介紹了很多種回收算法,每一種算法都有自己的優(yōu)點也有缺點,誰都不能替代誰,所以根據(jù)垃圾回收對象的特點進(jìn)行選擇,才是明智的選擇。

分代算法其實就是這樣的,根據(jù)回收對象的特點進(jìn)行選擇,在jvm中,年輕代適合使用復(fù)制算法,老年代適合使用標(biāo)記清除或標(biāo)記壓縮算法。

3 垃圾收集器以及內(nèi)存分配

在jvm中,實現(xiàn)了多種垃圾收集器,包括 :串行垃圾收集器、并行垃圾收集器、CMS(并發(fā))垃圾收集器、G1垃圾收集器。

3.1 串行垃圾收集器

串行垃圾收集器,是指使用單線程進(jìn)行垃圾回收,垃圾回收時,只有一個線程在工作,并且java應(yīng)用中的所有線程都要暫停,等待垃圾回收的完成。這種現(xiàn)象稱之為STW (Stop-The-World)

對于交互性較強的應(yīng)用而言,這種垃圾收集器是不能夠接受的。

一般在javaweb應(yīng)用中是不會采用該收集器的。

3.1.1 編寫測試代碼

3.1.2 設(shè)置垃圾回收為串行收集器

在程序運行參數(shù)中添加2個參數(shù),如下 :

-XX:+UseSerialGC : 指定年輕代和老年代都使用串行垃圾收集器

-XX:+PrintGCDetails : 打印垃圾回收的詳細(xì)信息

為了測試GC,將堆的初始和最大內(nèi)存都設(shè)置為16M

-XX:+UseSerialGC -XX:+PrintGCDetails -Xms16m -Xmx16m

啟動程序,可以看到下面信息 :

GC日志信息解讀 :

年輕代的內(nèi)存GC前后的大小 :

DefNew : 表示使用的是串行垃圾收集器。

Allocation Failure : 表示內(nèi)存分配失敗。

4416K -> 512K(4928K) : 表示,年輕代GC前,占有4416K內(nèi)存,GC后,占有512K內(nèi)存,總大小4928K。

0.0046102 secs : 表示GC所用的時間,單位為毫秒。

4416K->1973K(15872K) : 表示,GC前,堆內(nèi)存占有4416K,GC后,占有1973K,總大小為15872K。

Full GC :表示,內(nèi)存空間全部進(jìn)行GC

3.2 并行垃圾收集器

并行垃圾收集器在串行垃圾收集器的基礎(chǔ)之上做了改進(jìn),將單線程改為了多線程進(jìn)行垃圾回收,這樣可以縮短垃圾回收的時間。(這里是指,

并行能力較強的機器)

當(dāng)然了,并行垃圾收集器在收集的過程中也會暫停應(yīng)用程序,這個和串行垃圾回收器是一樣的,只是并行執(zhí)行,速度更快些,暫停的時間

更短一些。

3.2.1 ParNew垃圾收集器

ParNew垃圾收集器是僅僅工作在年輕代上,只是將串行的垃圾收集器改為了并行。

通過-XX:+UseParNewGC參數(shù)設(shè)置年輕代使用ParNew回收器,老年代使用的依然是串行收集器。

參數(shù) :

-XX:+UseParNewGC -XX:+PrintGCDetails -Xms16m -Xmx16m

打印出的信息

由以上信息可以看出,ParNew : 使用的是ParNew收集器。其他信息和串行收集器一致。

3.2.2 ParallelGC垃圾收集器

ParallelGC收集器工作機制和ParNewGC收集器一樣,只是在此基礎(chǔ)之上新增了兩個和系統(tǒng)吞吐量相關(guān)的參數(shù),使得其使用起來更加的靈

活和高效。

相關(guān)參數(shù)如下 :

-XX:+UseParellelGC

年輕代使用ParallelGC垃圾回收器,老年代使用串行回收器。

-XX:+UseParallelOldGC

年輕代使用ParallelGC垃圾回收器,老年代使用ParallelOldGC垃圾回收器。

-XX:MaxGCPauseMillis

設(shè)置最大的垃圾收集時的停頓時間,單位為毫秒。

需要注意的是,ParallelGC為了達(dá)到設(shè)置的停頓時間,可能會調(diào)整堆大小或其他的參數(shù),如果堆的大小設(shè)置的較小,就會導(dǎo)致GC工作

變的很頻繁,反而可能會影響到性能。

該參數(shù)使用需謹(jǐn)慎。

-XX:+GCTimeRatio

設(shè)置垃圾回收時間占程序運行時間的百分比,公式為1/(1 + n)。

它的值為0 ~ 100之間的數(shù)字,默認(rèn)值是99,也就是垃圾回收時間不能超過1%。

-XX:UseAdaptiveSizePolicy

自適應(yīng)GC模式,垃圾回收器將自動調(diào)整新生代、老年代等參數(shù),達(dá)到吞吐量、堆大小、停頓時間之間的平衡。

一般用于,手動調(diào)整參數(shù)比較困難的場景,讓收集器自動進(jìn)行調(diào)整。

參數(shù) :

-XX:+UseParallelGC

-XX:+UseParallelOldGC

-XX:MaxGCPauseMillis=100

-XX:+PrintGCDetails

-Xms16m

-Xmx16m

有以上信息可以看出,年輕代和老年代都使用了ParallelGC垃圾回收器。

3.3 CMS垃圾收集器

CMS全稱Concurrent Mark Sweep,是一款并發(fā)的、使用標(biāo)記-清除算法的垃圾回收器,該回收器是針對老年代垃圾回收的,通過參數(shù)-XX:+

UseConcMarkSweepGC進(jìn)行設(shè)置。

CMS垃圾回收器的執(zhí)行過程如下 :

初始化標(biāo)記(CMS-initial-mark),標(biāo)記root,會導(dǎo)致stw;

并發(fā)標(biāo)記(CMS-concurrent-mark),與用戶線程同時運行;

預(yù)清理(CMS-concurrent-preclean),與用戶線程同時運行;

重新標(biāo)記(CMS-remark),會導(dǎo)致stw;

并發(fā)清除(CMS-concurrent-sweep),與用戶線程同時運行;

調(diào)整堆大小,設(shè)置CMS在清理之后進(jìn)行內(nèi)存壓縮,目的是清理內(nèi)存中的碎片;

并發(fā)重置狀態(tài)等待下次CMS的觸發(fā)(CMS-concurrent-reset),與用戶線程同時運行;

3.3.1 測試

設(shè)置啟動參數(shù)

-XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xms16m -Xmx16m

由以上日志信息,可以看出CMS執(zhí)行的過程。

3.4 G1垃圾收集器(重點)

G1垃圾收集器是在jdk1.7中正式使用的全新的垃圾收集器,oracle官方計劃在jdk9中將G1變成默認(rèn)的垃圾

收集器,以替代CMS。

G1的設(shè)計原則就是簡化JVM性能調(diào)優(yōu),開發(fā)人員只需要簡單的三步即可完成調(diào)優(yōu) :

1. 第一步,開啟G1垃圾收集器

2. 第二步,設(shè)置堆的最大內(nèi)存

3. 第三部,設(shè)置最大的停頓時間

G1中提供了三種模式垃圾回收模式,Young GC、Mixed GC和Full GC,在不同的條件下被觸發(fā)。

3.4.1 原理

G1垃圾收集器相對比其他收集器而言,最大的區(qū)別在于它取消了年輕代、老年代的物理劃分,取而代之的是將堆劃分為若

干個區(qū)域(Region),這些區(qū)域中包含了有邏輯上的年輕代、老年代區(qū)域。

這樣做的好處就是,我們再也不用單獨的空間對每個代進(jìn)行設(shè)置了,不用擔(dān)心每個代內(nèi)存是否足夠。

在G1劃分的區(qū)域中,年輕代的垃圾收集依然采用暫停所有應(yīng)用線程的方式,將存活對象拷貝到老年代或者Survivor空間,G1

收集器通過將對象從一個區(qū)域復(fù)制到另外一個區(qū)域,完成了清理工作。

這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms內(nèi)存碎片問題的存在了。

在G1,有一個特殊的區(qū)域,叫Humongous區(qū)域。

如果一個對象占用的空間超過了分區(qū)容量50%以上,G1收集器就認(rèn)為這是一個巨型對象。

這些巨型對象,默認(rèn)直接會被分配在老年代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成影響。

為了解決這個問題,G1劃分了一個Humongous區(qū),它用來專門存放巨型對象。如果一個H區(qū)裝不下一個巨型對象,那么G1

會尋找連續(xù)的H分區(qū)來存儲。為了能找到連續(xù)的H區(qū),有時候不得不啟動Full GC。

3.4.2 Young GC

Young GC主要是對Eden區(qū)進(jìn)行GC,它在Eden空間耗盡時會被觸發(fā)。

Eden空間的數(shù)據(jù)移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分?jǐn)?shù)據(jù)會直接晉升到年老代空間。

Survivor區(qū)的數(shù)據(jù)移動到新的Survivor區(qū)中,也有部分?jǐn)?shù)據(jù)晉升到老年代空間中。

最終Eden空間的數(shù)據(jù)為空,GC停止工作,應(yīng)用線程繼續(xù)執(zhí)行。

3.4.2.1 Remembered Set(已記憶集合)

在GC年輕代的對象時,我們?nèi)绾握业侥贻p代中對象的根對象呢?

根對象可能是在年輕代中,也可以在老年代中,那么老年代中的所有對象都是根么?

如果全量掃描老年代,那么這樣掃描下來會耗費大量的時間。

于是,G1引進(jìn)了RSet的概念。它的全稱是Remenbreed Set,其作用是跟蹤指向某個堆內(nèi)的對象引用。

每個Region初始化時,會初始化一個RSet,該集合用來記錄并跟蹤其它Region指向該Region中對象的引用,每個Region默認(rèn)

按照512kb劃分成多個Card,所以RSet需要記錄的東西應(yīng)該是xx Region的xx Card。

每個RSet集合就是記錄每個Region中對象被引用的信息。這樣尋找根對象時直接掃描RSet集合就行。

3.4.3 Mixed GC

當(dāng)越來越多的對象晉升到老年代old region時,為了避免堆內(nèi)存被耗盡,虛擬機會觸發(fā)一個混合的垃圾收集器,即Mixed GC,

該算法并不是一個Old GC,除了回收整個YoungRegin,還會回收一部分的Old Region,這里需要注意 :是一部分老年代,而

不是全部老年代,可以選擇那些old region進(jìn)行收集,從而可以對垃圾回收的耗時時間進(jìn)行控制。也要注意的是Mixed GC并不是

Full GC。

Mixed GC什么時候觸發(fā)?由參賽-XX:InitiatingHeapOccupancyPercent=n 決定。默認(rèn) :45%,該參數(shù)的意思是 :當(dāng)老年代大小

占整個堆大小百分比達(dá)到該閥值時觸發(fā)。

它的GC步驟分2步 :

1 . 全局并發(fā)標(biāo)記(global concurrent marking)

2 . 拷貝存活對象(evacuation)

3.4.3.1 全局并發(fā)標(biāo)記

全局并發(fā)標(biāo)記,執(zhí)行過程分為五個步驟 :

初始標(biāo)記(initial mark,STW)

標(biāo)記從根節(jié)點直接可達(dá)的對象,這個階段會執(zhí)行一次年輕代GC,會產(chǎn)生全局停頓。

根區(qū)域掃描(root region scan)

G1 GC在初始標(biāo)記的存活區(qū)掃描對老年代的引用,并標(biāo)記被引用的對象。

該階段與應(yīng)用程序(非STW)同時運行,并且只有完成該階段后,才能開始下一次STW年輕代垃圾回收。

并發(fā)標(biāo)記(Concurrent Marking)

G1 GC在整個堆中查找可訪問的(存活的)對象。該階段與應(yīng)用程序同時運行,可以被STW年輕代垃圾回收中斷。

重新標(biāo)記(Renark,STW)

該階段是STW回收,因為程序在運行,針對上一次的標(biāo)記進(jìn)行修正。

清除垃圾(Cleanup,STW)

清除和重置標(biāo)記狀態(tài),該階段會STW,這個階段并不會實際上去做垃圾的收集,等待evacuation 階段來回收。

3.4.3.2 拷貝存活對象

Evacuation階段是全暫停的。該階段把一部分Region里的活對象拷貝到另一部分Region中,從而實現(xiàn)垃圾的回收清理。

3.4.4 G1收集器相關(guān)參數(shù)

-XX:+UseG1GC

使用G1垃圾收集器

-XX:MaxGCPauseMillis

設(shè)置期望達(dá)到的最大GC停頓時間指標(biāo)(JVM會盡力實現(xiàn),但不保證達(dá)到),默認(rèn)值是200毫秒。

-XX:G1HeapRegionSize=n

設(shè)置的G1區(qū)域的大小。值時2的冪,范圍是1MB到32MB之間。目標(biāo)是根據(jù)最小的Java堆大小劃分出約2048個區(qū)域。

默認(rèn)是堆內(nèi)存的1/2000。

-XX:ConcGCThreads=n

設(shè)置并行標(biāo)記的線程數(shù)。將n設(shè)置為并行垃圾回收線程數(shù)(ParallelGCThreads)的1/4左右。

-XX:InitiatingHeapOccupancyPercent=n

設(shè)置觸發(fā)標(biāo)記周期的Java堆占有率閥值。默認(rèn)占用率是整個Java堆的45%。

3.4.5 測試

-XX:+UseG!GC -XX:MaxGCPauseMillis=100 -XX:+PrintGCDetails -Xmx256m

3.4.6 對于G1垃圾收集器優(yōu)化建議

年輕代大小

避免使用-Xmn選項或-XX:NewRatio等其他相關(guān)選項顯示設(shè)置年輕代大小。

固定年輕代的大小會覆蓋暫停時間目標(biāo)。

暫停時間目標(biāo)不要太過嚴(yán)苛

G1 GC的吞吐量目標(biāo)是90%的應(yīng)用程序時間和10%的垃圾回收時間。

評估G1 GC的吞吐量時,暫停時間目標(biāo)不要太嚴(yán)苛。目標(biāo)太多嚴(yán)苛表示您愿意承受更多的垃圾回收開銷,而這會直接影響到吞吐量。

4 可視化GC日志分析工具

4.1 GC日志輸出參數(shù)

前面通過-XX:+PrintGCDetail可以對GC日志進(jìn)行打印,我們就可以在控制臺查看,這樣雖然可以查看GC的信息,但是并不直觀,可以借助于

第三方的GC的日志分析工具進(jìn)行查看。

在日志打印輸出設(shè)計到的參數(shù)如下 :

-XX:+PrintGC 輸出GC日志

-XX:+PrintGCDetails 輸出GC的詳細(xì)日志

-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準(zhǔn)時間的形式)

-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如2013-05-04T21:53:59.234+0800)

-XX:+PrintHeapAtGC 在進(jìn)行GC的前后打印出堆的信息

-Xloggc:../logs/gc.log 日志文件的輸出路徑

測試 :

-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xmx256m -XX:+PrintGCDetails

-XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC

-Xloggc:F://test//gc.log

運行后就可以在F盤下生成gc.log文件。

4.2 GC Easy可視化工具

GC Easy是一款在線的可視化工具,易用、功能強大,網(wǎng)站 :https://gceasy.io/

這個是顯示JVM堆的總大小、年輕代大小、老年代大小。

這個是顯示GC停頓時間和吞吐率

各個GC執(zhí)行情況

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

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

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