簡單聊聊JVM中的幾種垃圾收集算法
在之前的文章中,我們介紹了對(duì)象的創(chuàng)建過程,以及運(yùn)行期的相關(guān)優(yōu)化手段。本文主要介紹對(duì)象回收的判定方式以及垃圾對(duì)象的回收算法等相關(guān)知識(shí)。
下面我們一起來了解一下。
當(dāng)一個(gè)對(duì)象被創(chuàng)建時(shí),虛擬機(jī)會(huì)優(yōu)先分配到堆空間中,當(dāng)對(duì)象不再被使用了,虛擬機(jī)會(huì)對(duì)其進(jìn)行回收處理,以便釋放內(nèi)存空間,這個(gè)過程也被稱為垃圾對(duì)象回收。
那么如何找到對(duì)象是否可以進(jìn)行回收呢?一般有兩種方式。
引用計(jì)數(shù)法
可達(dá)性分析法
下面我們一起來了解下相關(guān)知識(shí)。
2.1、引用計(jì)數(shù)法
這個(gè)方法的實(shí)現(xiàn)思路是:在對(duì)象中維護(hù)一個(gè)引用計(jì)數(shù)器,每當(dāng)一個(gè)地方引用這個(gè)對(duì)象時(shí),計(jì)數(shù)器值+1;當(dāng)引用失效時(shí),計(jì)數(shù)器值-1。當(dāng)對(duì)象的計(jì)數(shù)器值為 0,表示這個(gè)對(duì)象不再被使用了,可以被回收。
這種方法使用場景很多,但很少有垃圾收集器會(huì)使用這種方式。
原因在于:這種方式存在一個(gè)致命的缺陷,比如堆中的兩個(gè)對(duì)象相互引用,此時(shí)他們的計(jì)數(shù)器值是 1,但這兩個(gè)對(duì)象并沒有被外部使用,因此不會(huì)被回收,容易造成內(nèi)存泄露。
2.2、可達(dá)性分析法
這個(gè)方法的實(shí)現(xiàn)思路是:從“GC Roots”(這個(gè) GC Roots 可以是棧中的引用變量,也可以是方法區(qū)的引用變量或常量)開始掃描堆中的對(duì)象,沿著 GC Roots 一路掃描,被掃描的所有對(duì)象全部標(biāo)記為存活對(duì)象;掃描完成之后,沒有被標(biāo)記的視為垃圾對(duì)象,可以被回收。
比如對(duì)象 A 被線程占中的變量 a 引用著,對(duì)象 A 中引用著對(duì)象 B,對(duì)象 B 又引用著 C 等,沿著 a 開始掃描,會(huì)掃描到對(duì)象 A,B,C 等,并把它們標(biāo)記為存活對(duì)象。全部掃描完成之后,當(dāng)一個(gè)對(duì)象到 GC Roots 沒有任何引用鏈時(shí),表示此對(duì)象是不可用的,等待被 GC 回收。
圖片
在 JVM 中,可以作為 GC Roots 的對(duì)象包括:
- 虛擬機(jī)棧中引用的對(duì)象
- 方法區(qū)中靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- 本地方法棧中 JNI(即 Native 方法)引用的對(duì)象
三、垃圾回收算法
當(dāng)一個(gè)對(duì)象被判定為垃圾對(duì)象之后,剩下的工作就是如何進(jìn)行回收了。
下面我們一起來看看常見的幾種垃圾回收算法的思想。
3.1、標(biāo)記-清除算法
標(biāo)記-清除算法如同它的名字一樣,分為“標(biāo)記”和“清除”兩個(gè)階段,也是最基礎(chǔ)的算法。
首先標(biāo)記出所有需要回收的對(duì)象,標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。之所以說它是最基礎(chǔ)的收集算法,是因?yàn)楹罄m(xù)的收集算法都是基于這種思路并對(duì)其缺點(diǎn)進(jìn)行改進(jìn)而得到的。
這個(gè)算法也有很多的不足,主要體現(xiàn)在效率和空間。
- 從效率的角度講,標(biāo)記和清除兩個(gè)過程的效率都不高;
- 從空間的角度講,標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致后面的程序運(yùn)行過程中分配較大對(duì)象時(shí),無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)一次垃圾收集動(dòng)作
標(biāo)記-清除算法執(zhí)行過程,可以用如下圖來概括:
圖片
3.2、復(fù)制算法
復(fù)制算法是為了解決效率問題而出現(xiàn)的,它將可用的內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
這樣每次只需要對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也不需要考慮內(nèi)存碎片等復(fù)雜情況,只需要移動(dòng)指針,按照順序分配即可。
這個(gè)算法也有缺點(diǎn),操作的時(shí)候內(nèi)存會(huì)縮小為了原來的一半,代價(jià)很高;其次,持續(xù)復(fù)制長生存期的對(duì)象會(huì)導(dǎo)致回收效果不佳,效率較低。
一般的商用虛擬機(jī)會(huì)采用這種算法來回收新生代(也稱為年輕代)的對(duì)象,不過研究表明1:1的比例不是很科學(xué),因此新生代的內(nèi)存空間被細(xì)劃分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor;每次回收時(shí),將 Eden 和 Survivor 空間中還存活的對(duì)象一次性復(fù)制到另外一塊 Survivor 空間上,最后清理掉之前的 Eden 和 Survivor 空間。
HotSpot 虛擬機(jī)默認(rèn) Eden 和 Survivor 區(qū)的比例是8 : 1 : 1,期望每次回收后只有不到 10% 的對(duì)象存活,如果出現(xiàn) Survivor 空間不夠用時(shí),需要依賴?yán)夏甏M(jìn)行分配擔(dān)保。
復(fù)制算法執(zhí)行過程,可以用如下圖來概括:
圖片
3.3、標(biāo)記-壓縮算法
在上面我們提到了復(fù)制算法的優(yōu)點(diǎn)和缺點(diǎn),針對(duì)對(duì)象存活率較高的場景,進(jìn)行大量的復(fù)制操作時(shí),效率很低下。如果不想浪費(fèi) 50% 的空間,當(dāng)對(duì)象 100% 存活時(shí),那么需要有額外的空間進(jìn)行分配擔(dān)保。
在 HotSpot 虛擬機(jī)中,堆空間劃分成兩個(gè)不同的區(qū)域:新生代和老年代,目的是為了更有效率的回收對(duì)象。新生代的對(duì)象存活率低,會(huì)優(yōu)先被回收,如果多次執(zhí)行依然沒有被回收,就會(huì)轉(zhuǎn)移到老年代。老年代都是不易被回收的對(duì)象,對(duì)象存活率高,因此一般不能直接選用復(fù)制算法。
根據(jù)老年代的特點(diǎn),有人提出了另外一種標(biāo)記-整理算法,也稱為標(biāo)記-壓縮算法,過程與標(biāo)記-清除算法一樣,不過不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活對(duì)象都向一端移動(dòng),然后直接清理掉邊界外的內(nèi)存。
標(biāo)記-整理算法執(zhí)行過程,可以用如下圖來概括:
圖片
3.4、分代收集算法
分代收集算法,可以看成以上內(nèi)容的延伸。它的實(shí)現(xiàn)思路是根據(jù)對(duì)象的生命周期的不同,將內(nèi)存劃分為幾塊,比如把堆空間劃分為新生代和老年代,然后根據(jù)各塊的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/p>
在新生代中,存在大批對(duì)象死去、少量對(duì)象存活的特點(diǎn),會(huì)采用“復(fù)制算法”,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成垃圾對(duì)象收集,效率高;在老年代中,存在對(duì)象存活率高、沒有額外空間對(duì)它進(jìn)行分配擔(dān)保的特點(diǎn),會(huì)采用“標(biāo)記-清理”或者“標(biāo)記-整理”算法來進(jìn)行回收。
可以用如下圖來概括堆內(nèi)存的空間布局:
圖片
四、垃圾收集器
如果說收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實(shí)現(xiàn)。
不同的虛擬機(jī)所提供的垃圾收集器可能會(huì)有很大差異,以 HotSpot 虛擬機(jī)為例,所包含的垃圾收集器可以用如下圖來概括。
圖片
上圖中的連線表示,不同分代的收集器可以搭配使用。
- 新生代收集器:Serial、ParNew、Parallel Scavenge
- 老年代收集器:Serial Old、CMS、Parallel Old
- 通用收集器: G1
在虛擬機(jī)中,沒有所謂的萬能收集器,只有根據(jù)具體的業(yè)務(wù)場景,選擇最合適的收集器。這也是為什么 HotSpot 實(shí)現(xiàn)了這么多收集器的原因。
下面我們一起來看看相關(guān)的具體實(shí)現(xiàn)。
4.1、Serial 和 Serial Old收集器
Serial 系列的垃圾收集器是 JVM 的第一款收集器,它的設(shè)計(jì)思路很簡單,在新生代,使用單線程采用復(fù)制算法進(jìn)行收集對(duì)象;在老年代,使用單線程采用標(biāo)記整理算法進(jìn)行收集對(duì)象;垃圾收集的過程中會(huì)暫停用戶線程,直到垃圾收集完畢。
因?yàn)楫?dāng)時(shí)的硬件環(huán)境配置都不高,內(nèi)存都是幾十兆,CPU 也都是單核的,不像現(xiàn)在這樣處處都是高并發(fā)的應(yīng)用場景。限于當(dāng)時(shí)的硬件資源和應(yīng)用場景,這個(gè)收集器優(yōu)勢很突出,簡單高效、消耗資源也很少。
唯一的不足在于,在用戶不可見的情況下要把用戶正常工作的線程全部停掉,這對(duì)很多應(yīng)用比較難以接受。不過實(shí)際上到目前為止,Serial 收集器依然是虛擬機(jī)在 Client 模式下運(yùn)行的默認(rèn)新生代收集器,因?yàn)樗唵味咝???蛻舳藨?yīng)用模型下,分配給虛擬機(jī)管理的內(nèi)存一般來說不會(huì)很大,收集幾十兆甚至一兩百兆的新生代對(duì)象,停頓時(shí)間平均在幾十毫秒,只要不是頻繁收集,完全可以接受。
整個(gè)流程,可以用如下圖來概括。
圖片
總結(jié)下來,收集器特點(diǎn)如下:
- 收集區(qū)域: Serial(新生代),Serial Old(老年代)
- 收集算法: Serial(復(fù)制算法),Serial Old(標(biāo)記整理算法)
- 收集方式:單線程
- 優(yōu)勢:簡單高效,內(nèi)存資源占用少,單核 CPU 環(huán)境最佳選項(xiàng)
- 劣勢:整個(gè)搜集過程需要停頓用戶線程,多核 CPU、大內(nèi)存的環(huán)境,資源優(yōu)勢無法發(fā)揮起來
4.2、ParNew收集器
ParNew 收集器,可以看成是 Serial 收集器的多線程版本。除了使用多線程進(jìn)行垃圾收集外,其余行為和 Serial 收集器完全一樣,包括使用的也是復(fù)制算法,垃圾收集時(shí)暫停用戶線程。在多核 CPU 資源環(huán)境下,可以顯著提升整個(gè)垃圾收集的性能,也是虛擬機(jī)在 Server 模式下運(yùn)行的首選新生代收集器。
能讓 ParNew 出名的一個(gè)核心因素是,它是除了 Serial 收集器外,目前唯一一個(gè)能與 CMS 收集器配合一起使用的新生代收集器,因?yàn)?CMS 優(yōu)秀所以 ParNew 也出名了,有點(diǎn)類似碰上了大款的感覺,其中 CMS 收集器是一款幾乎可以認(rèn)為有劃時(shí)代意義的垃圾收集器,下文我們?cè)僦v。
其次,ParNew 收集器在單個(gè) CPU 的環(huán)境中絕對(duì)不會(huì)有比 Serial 收集器更好的效果,甚至由于線程交互的開銷,該收集器在兩個(gè) CPU 的環(huán)境中都不能百分之百保證可以超越 Serial 收集器。當(dāng)然,隨著可用 CPU 數(shù)量的增加,它對(duì)于垃圾收集的效率提升還是很有幫助的。
整個(gè)流程,可以用如下圖來概括。
圖片
總結(jié)下來,收集器特點(diǎn)如下:
- 收集區(qū)域:新生代
- 收集算法:復(fù)制算法
- 收集方式:多線程
- 優(yōu)勢:多線程收集,多核 CPU 環(huán)境下效率要比 serial 高,新生代中,除了 Serial 收集器外目前唯一一個(gè)能與 CMS 配合的收集器
- 劣勢:整個(gè)搜集過程需要停頓用戶線程
4.3、Parallel Scavenge 和 Parallel Old收集器
Parallel Scavenge 和 ParNew 收集器很類似,也是一款使用多線程采用復(fù)制算法的新生代收集器;Parallel Old 收集器是一款使用多線程采用標(biāo)記整理算法的老年代收集器;垃圾收集過程中也會(huì)暫停用戶線程,直到整個(gè)垃圾收集過程結(jié)束。
不同的是,Parallel 收集器更關(guān)注系統(tǒng)的吞吐量,也被稱為“吞吐量優(yōu)先收集器”。
所謂吞吐量的意思就是 CPU 用于運(yùn)行用戶代碼時(shí)間與 CPU 總消耗時(shí)間的比值,即吞吐量=運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收集時(shí)間),比如虛擬機(jī)總運(yùn)行 100 分鐘,垃圾收集 1 分鐘,那吞吐量就是 99%。高吞吐量可以高效率的利用 CPU 資源,盡快完成程序的運(yùn)算任務(wù),主要適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)。
自適應(yīng)調(diào)節(jié)策略也是 Parallel Scavenge 與 ParNew 的一個(gè)重要區(qū)別,用戶可以通過參數(shù)來打開自適應(yīng)調(diào)節(jié)策略,比如-XX:+UseAdaptiveSizePolicy參數(shù),打開之后就不需要手動(dòng)指定新生代大小、Eden 區(qū)和 Survivor 參數(shù)等細(xì)節(jié)參數(shù)了,虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或最大的吞吐量。如果對(duì)于垃圾收集器運(yùn)作原理不太了解,優(yōu)化比較困難的情況下,使用 Parallel 收集器配合自適應(yīng)調(diào)節(jié)策略,把內(nèi)存管理的調(diào)優(yōu)任務(wù)交給虛擬機(jī)去完成也是一個(gè)不錯(cuò)的選擇。
另外,Parallel 收集器是虛擬機(jī)在 Server 模式下運(yùn)行的默認(rèn)垃圾收集器。
整個(gè)執(zhí)行流程,跟 ParNew 收集器類似。
總結(jié)下來,收集器特點(diǎn)如下:
- 收集區(qū)域:Parallel Scavenge(新生代),Parallel Old(老年代)
- 收集算法:Parallel Scavenge(復(fù)制算法),Parallel Old(標(biāo)記整理算法)
- 收集方式:多線程
- 優(yōu)勢:多線程收集,多核 CPU 環(huán)境下效率要比 serial 高
- 劣勢:整個(gè)搜集過程需要停頓用戶線程
4.4、CMS收集器
CMS 收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的老年代收集器。
與前面幾個(gè)收集器不同,它采用了一種全新的策略可以在垃圾回收過程中的某些階段用戶線程和垃圾回收線程一起工作,從而避免了因?yàn)殚L時(shí)間的垃圾回收而使用戶線程一直處于等待之中。
目前很大一部分 Java 應(yīng)用集中在互聯(lián)網(wǎng)站或者 B/S 系統(tǒng)的服務(wù)端上,這類應(yīng)用尤其注重服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間最短,比如在一個(gè)長度為 M 毫秒的時(shí)間片段內(nèi),消耗在垃圾收集上的時(shí)間不得超過多少毫秒,以期給用戶帶來較好的體驗(yàn),其中 CMS 收集器就非常符合這類應(yīng)用的需求。
CMS 的英文全程是:Concurrent Mark-Sweep Collector,從名字上就能看出 CMS 收集器是基于“標(biāo)記-清除”算法實(shí)現(xiàn)的,它的運(yùn)作過程相對(duì)于前面幾種收集器來說要更復(fù)雜一些,整個(gè)過程分為如下 4 個(gè)步驟:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 重新標(biāo)記
- 并發(fā)清除
CMS 會(huì)根據(jù)每個(gè)階段不同的特性來決定是否停頓用戶線程。整個(gè)流程,可以用如下圖來概括。(圖片來自于勤勞的小手 - 垃圾收集器文章)
圖片
4.4.1、階段一:初始標(biāo)記
初始標(biāo)記階段的工作主要是標(biāo)記一下 GC Roots 能直接關(guān)聯(lián)到的對(duì)象,這個(gè)過程會(huì)短暫的停頓用戶線程,因?yàn)椴⒉粫?huì)對(duì)整個(gè) GC Roots 的引用進(jìn)行遍歷,因此速度很快。
4.4.2、階段二:并發(fā)標(biāo)記
并發(fā)標(biāo)記階段的工作主要是把階段一標(biāo)記好的 GC Roots 對(duì)象進(jìn)行深度的遍歷,找到所有與 GC Roots 關(guān)聯(lián)的對(duì)象并進(jìn)行標(biāo)記,這個(gè)過程會(huì)采用多線程的方式進(jìn)行遍歷標(biāo)記,因?yàn)榉浅:臅r(shí),CMS 考慮到為了盡量不停頓用戶線程,因此這個(gè)階段不會(huì)暫停用戶線程,也就是說,此時(shí) JVM 會(huì)分配一些資源給用戶線程執(zhí)行任務(wù),通過這樣的方式減少用戶線程的停頓時(shí)間。
4.4.3、階段三:重新標(biāo)記
重新標(biāo)記階段的工作主要是修補(bǔ)階段二用戶線程運(yùn)行期間產(chǎn)生新的垃圾對(duì)象,進(jìn)行重新標(biāo)記,同樣也是采用多線程方式進(jìn)行,此階段數(shù)量不會(huì)很多,會(huì)短暫的停頓用戶線程,速度也很快。
4.4.4、階段四:并發(fā)清除
并發(fā)清除階段的工作主要是對(duì)那些被標(biāo)記為可回收的對(duì)象進(jìn)行清理,在一般情況下,并發(fā)清除階段是使用的是“標(biāo)記-清除”算法,因?yàn)檫@個(gè)過程不會(huì)牽扯到對(duì)象的地址變更,所以 CMS 在并發(fā)清除階段是不需要停止用戶線程的,對(duì)象回收效率非常高。
與此同時(shí),正因?yàn)椴l(fā)清除階段用戶線程也可以同時(shí)運(yùn)行,所以在用戶線程運(yùn)行的過程中自然也會(huì)產(chǎn)生新的垃圾對(duì)象,這也就是導(dǎo)致 CMS 收集器會(huì)產(chǎn)生“浮動(dòng)垃圾”的原因,此時(shí)也會(huì)產(chǎn)生很多的空間碎片,當(dāng)空間碎片到達(dá)了一定程度時(shí),此時(shí) CMS 就會(huì)使用“標(biāo)記-整理”算法來解決空間碎片的問題。
在上文的垃圾回收算法中我們有說到,“標(biāo)記-整理”算法會(huì)將對(duì)象的位置進(jìn)行挪動(dòng)并更新對(duì)象的引用的指向地址,在這個(gè)過程中,如果用戶線程同時(shí)運(yùn)行的話會(huì)產(chǎn)生并發(fā)問題,因此當(dāng) CMS 進(jìn)行碎片整理的時(shí)候必須得停止用戶線程。所以,在某些情況下,并發(fā)清除階段 CMS 也會(huì)停頓用戶線程。
CMS 收集器作為一個(gè)全新思路的垃圾收集器,雖然很優(yōu)秀,但一直沒有被 Hospot 虛擬機(jī)納入為默認(rèn)的垃圾收集器。時(shí)至今日,JDK1.8 使用的默認(rèn)收集器都還是 Parallel scavenge 和 Parallel old 收集器,主要原因在于 CMS 存在一些比較頭疼的問題,比如浮動(dòng)垃圾、空間碎片整理時(shí)會(huì)造成系統(tǒng)卡頓、在并發(fā)清除階段可能會(huì)出現(xiàn)系統(tǒng)長時(shí)間的假死。
4.4.5、小結(jié)
總結(jié)下來,收集器特點(diǎn)如下:
- 收集區(qū)域:老年代
- 收集算法:標(biāo)記清除算法 + 標(biāo)記整理算法
- 收集方式:多線程
- 優(yōu)勢:多線程收集過程中可以做到不停止用戶線程,以獲取最短回收停頓時(shí)間
- 劣勢:會(huì)產(chǎn)生浮動(dòng)垃圾、空間碎片整理時(shí)會(huì)造成系統(tǒng)卡頓、并發(fā)清除階段可能會(huì)出現(xiàn)系統(tǒng)假死等問題
4.5、G1收集器
G1(Garbage-First)收集器是當(dāng)今收集器技術(shù)發(fā)展的最前沿成果之一,從 JDK 7 Update 4 后開始進(jìn)入商用。
在 G1 收集器出現(xiàn)之前,不管是 Serial 系列,Parallel 系列,還是 CMS 收集器,它們都是基于把內(nèi)存進(jìn)行物理分區(qū)的形式將 JVM 內(nèi)存分成新生代、老年代、永久代或 MetaSpace,這種分區(qū)模式下進(jìn)行垃圾收集時(shí)必須對(duì)某個(gè)區(qū)域進(jìn)行整體性的收集,比如整個(gè)新生代、整個(gè)老年代收集或者整個(gè)堆,當(dāng)內(nèi)存空間不大的時(shí)候,比如幾個(gè) G,通過參數(shù)優(yōu)化能取得不錯(cuò)的收集性能。但是,隨著硬件資源的發(fā)展,JVM 可用內(nèi)存從幾十 G 到幾百 G 甚至上 T 時(shí),這種采用傳統(tǒng)模式下的物理分區(qū)進(jìn)行收集時(shí),每次掃描內(nèi)存的區(qū)域自然就變大了,進(jìn)行垃圾清理的時(shí)間自然就變得更長了,此時(shí)傳統(tǒng)的收集器即時(shí)再怎么優(yōu)化,也難以取得令人滿意的收集效果,因此需要一款全新的垃圾收集器。
G1 收集器就是在這樣的環(huán)境下誕生的,它摒棄了原來的物理分區(qū),把整個(gè) Java 堆分成若干個(gè)大小相等的獨(dú)立區(qū)域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離,它們都是一部分 Region 的集合。從結(jié)構(gòu)上看,G1 收集器不要求整個(gè)新生代或者老年代都是連續(xù)的,也不再堅(jiān)持固定大小和固定數(shù)量,它會(huì)跟蹤各個(gè) Region 里面的垃圾堆積的價(jià)值大小,在后臺(tái)維護(hù)一個(gè)優(yōu)先列表,每次根據(jù)允許的收集時(shí)間,優(yōu)先回收價(jià)值最大的 Region。這種通過 Region 劃分內(nèi)存空間以及有優(yōu)先級(jí)的區(qū)域回收方式,保證了 G1 收集器在有限的時(shí)間內(nèi)可以獲取盡可能高的收集效率。
G1 收集器內(nèi)存劃分,可以用如下圖來概括。(圖片來自于勤勞的小手 - 垃圾收集器文章)
圖片
在 G1 收集器里面維護(hù)了一個(gè) Collect Set 集合,這個(gè)集合里面記錄了待回收的 Region 區(qū)域信息,同時(shí)也包括了每個(gè) Region 區(qū)域可回收的大小空間。通過 Collect Set 里面的信息,G1 在進(jìn)行垃圾收集時(shí),可以根據(jù)用戶設(shè)定的可接受停頓時(shí)間來進(jìn)行分析,在設(shè)定的時(shí)間范圍內(nèi)優(yōu)先收集垃圾最多的 Region 區(qū)域,以實(shí)現(xiàn)高吞吐、低停頓的收集效果。
在工作流程上,G1 收集器也吸收了 CMS 很多優(yōu)秀的收集思路,整個(gè)垃圾收集過程,可以分為如下 4 個(gè)步驟:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 重新標(biāo)記
- 篩選回收
G1 收集器的垃圾回收流程和 CMS 邏輯大致相同,主要的區(qū)別在最后一個(gè)階段,G1 不會(huì)直接進(jìn)行清除,而是會(huì)根據(jù)設(shè)置的停頓時(shí)間進(jìn)行智能的篩選和局部的回收,采用“標(biāo)記復(fù)制”算法來實(shí)現(xiàn)。
整個(gè)流程,可以用如下圖來概括。
圖片
4.5.1、階段一:初始標(biāo)記
此階段的工作內(nèi)容與上文介紹的 CMS 收集器一樣,會(huì)先把所有 GC Roots 直接引用的對(duì)象進(jìn)行標(biāo)記,同時(shí)會(huì)短暫的停止用戶線程,因?yàn)椴粫?huì)對(duì)整個(gè) GC Roots 的引用進(jìn)行遍歷,因此速度比較快。
4.5.2、階段二:并發(fā)標(biāo)記
此階段的工作內(nèi)容與上文介紹的 CMS 收集器也一樣,找到所有與 GC Roots 關(guān)聯(lián)的對(duì)象并進(jìn)行深度遍歷標(biāo)記,會(huì)采用多線程的方式進(jìn)行遍歷標(biāo)記,因?yàn)楸容^耗時(shí),為了盡量不停頓用戶線程,這個(gè)階段 GC 線程會(huì)和用戶線程同時(shí)運(yùn)行,通過這樣的方式減少用戶線程的停頓時(shí)間。
4.5.3、階段三:重新標(biāo)記
此階段的工作內(nèi)容與上文介紹的 CMS 收集器也是一樣,針對(duì)階段二用戶線程運(yùn)行的過程中產(chǎn)生新的垃圾,采用多線程方式進(jìn)行重新標(biāo)記,為了避免這個(gè)過程再次產(chǎn)生新的垃圾對(duì)象,會(huì)短暫的停止用戶線程,因?yàn)閿?shù)量不會(huì)很多,因此速度比較快。
4.5.4、階段四:篩選回收
篩選回收階段的工作主要是把存活的對(duì)象復(fù)制到 Region 空閑區(qū)域,同時(shí)會(huì)根據(jù) Collect Set 記錄的可回收 Region 信息進(jìn)行篩選,計(jì)算 Region 回收成本,接著根據(jù)用戶設(shè)定的停頓時(shí)間值制定回收計(jì)劃,最后根據(jù)回收計(jì)劃篩選合適的 Region 區(qū)域進(jìn)行垃圾回收。
從局部來看,G1 使用的是復(fù)制算法,將存活對(duì)象從一個(gè) Region 區(qū)域復(fù)制到另一個(gè) Region 空閑區(qū)域;但從整個(gè)堆來看,G1 使用的邏輯又相當(dāng)于標(biāo)記整理算法,每次垃圾收集時(shí)會(huì)把存活的對(duì)象整理到對(duì)應(yīng)可用的 Region 區(qū)域,再把原來的 Region 區(qū)域標(biāo)記為可回收區(qū)域并記錄到 Collect Set 中,因此 G1 的每一次回收都可以看作是一次標(biāo)記整理過程,兩者都不會(huì)產(chǎn)生空間碎片問題。
4.5.5、小結(jié)
總結(jié)下來,收集器特點(diǎn)如下:
- 收集區(qū)域:整個(gè)堆內(nèi)存
- 收集算法:復(fù)制算法
- 收集方式:多線程
- 優(yōu)勢:停頓時(shí)間可控,吞吐量高,不會(huì)產(chǎn)生空間碎片,不需要額外的收集器搭配
- 劣勢:目前而言,相較于 CMS,G1 還不具備全方位、壓倒性優(yōu)勢,G1 在收集過程中內(nèi)存占用和執(zhí)行負(fù)載都偏高;其次,在小內(nèi)存應(yīng)用上 CMS 的表現(xiàn)大概率會(huì)優(yōu)于 G1,而 G1 在大內(nèi)存應(yīng)用上會(huì)比較有優(yōu)勢,6G 以上的內(nèi)存可以考慮使用 G1 收集器
4.6、常用的收集器組合
最后我們對(duì)以上介紹的垃圾收集器進(jìn)行一次匯總,同時(shí)介紹一下服務(wù)器端常用的組合模式,內(nèi)容如下。
服務(wù)器組合 | 新生代收集器 | 老年代收集器 | 備注 |
組合一 | Serial | Serial Old | Serial 是一個(gè)使用單線程采用復(fù)制算法的新生代收集器;Serial Old 是一個(gè)使用單線程采用標(biāo)記整理算法的老年代收集器,GC 時(shí)會(huì)暫停所有應(yīng)用線程,可以使用-XX:+UseSerialGC選項(xiàng)來開啟 |
組合二 | ParNew | Serial Old | ParNew 是一個(gè)使用多線程采用復(fù)制算法的新生代收集器,GC 時(shí)會(huì)暫停所有應(yīng)用線程,可以使用-XX:+UseParNewGC選項(xiàng)來開啟 |
組合三 | Parallel Scavenge | Serial Old | Parallel Scavenge 是一個(gè)使用多線程采用復(fù)制算法的新生代收集器,GC 時(shí)會(huì)暫停所有應(yīng)用線程,可以使用-XX:+UseParallelGC選項(xiàng)來開啟;需要注意的是,在jdk1.7及之前的版本中,這個(gè)參數(shù)默認(rèn)采用 Serial Old 作為老年代收集器;在jdk1.8及之后的版本中,默認(rèn)采用 Parallel Old 作為老年代收集器 |
組合四 | Parallel Scavenge | Parallel Old | Parallel Old是 Serial Old 的多線程版收集器,可以使用-XX:+UseParallelOldGC選項(xiàng)來開啟 |
組合五 | Serial | CMS + Serial Old | CMS 是一個(gè)使用多線程采用標(biāo)記清楚算法的老年代收集器,可以實(shí)現(xiàn) GC 線程和用戶線程并發(fā)工作,不需要暫停所有用戶線程;另外,可以將 Serial Old 收集器作為備選,當(dāng) CMS 進(jìn)行 GC 失敗時(shí),會(huì)自動(dòng)使用 Serial Old 進(jìn)行 GC;可以使用-XX:+UseConcMarkSweepGC選項(xiàng)來開啟 |
組合六 | ParNew | CMS + Serial Old | ParNew 是除了 Serial 以外,唯一一個(gè)能搭配 CMS 的新生代收集器;可以使用-XX:+UseConcMarkSweepGC開啟,默認(rèn)使用 ParNew 作為新生代收集器,也可以通過-XX:+UseParNewGC強(qiáng)制指定 ParNew |
組合七 | G1 | G1 | G1 是一個(gè)新一代的垃圾收集器,摒棄了原來的物理分區(qū),把整個(gè) Java 堆分成若干個(gè)大小相等的獨(dú)立區(qū)域(Region),針對(duì)局部區(qū)域使用多線程采用復(fù)制算法進(jìn)行篩選回收,可以使用-XX:+UseG1GC選項(xiàng)來開啟 |
五、方法區(qū)回收
以上介紹的都是對(duì)象的回收過程,在之前的 JVM 內(nèi)存結(jié)構(gòu)的文章中我們介紹到,Java 應(yīng)用程序運(yùn)行時(shí),除了堆空間會(huì)存在垃圾數(shù)據(jù)以外,方法區(qū)同樣也存在。
雖然虛擬機(jī)規(guī)范中沒有明確要求方法區(qū)一定要實(shí)現(xiàn)垃圾回收,主要原因在于這個(gè)區(qū)域的垃圾回收效率非常低,但是 HotSpot 虛擬機(jī)對(duì)方法區(qū)也會(huì)進(jìn)行回收的,主要回收的是廢棄常量和無用的類兩部分。
如何判斷一個(gè)常量是否為“廢棄常量”呢?其實(shí)很簡單,只要當(dāng)前系統(tǒng)中沒有任何一處引用該常量,就會(huì)被判定為廢棄常量。
如何判斷一個(gè)類是否為“無用的類”呢?條件非常苛刻,需要同時(shí)滿足以下三點(diǎn)。
- 1.該類所有實(shí)例都已經(jīng)被回收,也就是說 Java 堆中不存在該類的任何實(shí)例
- 2.該類對(duì)應(yīng)的java.lang.Class對(duì)象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法
- 3.加載該類的 ClassLoader 已經(jīng)被回收,也就是說這個(gè)類的類加載器被卸載回收了
滿足以上三個(gè)條件則表示這個(gè)類再也無用了,HotSpot 虛擬機(jī)會(huì)對(duì)此類進(jìn)行回收。例如在大量使用反射、動(dòng)態(tài)代理、CGLib 等 ByteCode 框架,并自定義 ClassLoader 創(chuàng)建的類,為了保證方法區(qū)不會(huì)溢出,虛擬機(jī)會(huì)在適當(dāng)?shù)那闆r下對(duì)無用的類進(jìn)行回收。
在 JDK1.7 及以前的版本中,用永久代來作為方法區(qū)的實(shí)現(xiàn),當(dāng)永久代的空間不足時(shí)會(huì)觸發(fā) Full GC。
在 JDK1.8 及之后的版本中,用元空間來作為方法區(qū)的實(shí)現(xiàn),元空間的內(nèi)存空間默認(rèn)使用的是操作系統(tǒng)的內(nèi)存空間,它的垃圾回收不再由 Java 來控制,元空間的內(nèi)存管理由元空間虛擬機(jī)來完成。
六、小結(jié)
本文主要圍繞對(duì)象的回收判斷方式,垃圾回收算法以及垃圾收集器,做了一次知識(shí)內(nèi)容的整理和總結(jié),如果有描述不當(dāng)?shù)牡胤?,歡迎大家留言指出,不勝感激。
七、參考
1.https://zhuanlan.zhihu.com/p/267223891
2.https://www.cnblogs.com/xrq730/p/4836700.html
3.https://zhuanlan.zhihu.com/p/248709769
4.http://www.ityouknow.com/jvm/2017/09/28/jvm-overview.html