淺談JVM中的垃圾回收

作者:一字馬胡

轉(zhuǎn)載標(biāo)志 【2017-11-12】

更新日志

日期更新內(nèi)容備注

2017-11-12新建文章初版

導(dǎo)入

作為Java語(yǔ)言的使用者,不像C++那樣需要自己負(fù)責(zé)內(nèi)存的申請(qǐng)和釋放,因?yàn)镴ava語(yǔ)言有垃圾收集器(garbage collector GC),GC會(huì)負(fù)責(zé)將那些不再使用的對(duì)象釋放掉,所以一般情況下,JVM的內(nèi)存管理對(duì)開(kāi)發(fā)者是無(wú)感知的,我們只需要設(shè)置一些運(yùn)行時(shí)VM參數(shù)就可以了,但是,JVM發(fā)展至今,GC的類(lèi)型和數(shù)量已經(jīng)很豐富了,學(xué)習(xí)和理解GC是一個(gè)合格的Java開(kāi)發(fā)者的必修課,因?yàn)樵趯W(xué)習(xí)和理解了不同的GC之后,可以在合適的場(chǎng)景下選擇合適的GC,不同的GC適用于不同的場(chǎng)景,并且不同的應(yīng)用相同的GC也需要設(shè)置不同的參數(shù),不理解GC或者參數(shù),就無(wú)法正確設(shè)置以獲得最佳性能。本文并不會(huì)對(duì)GC做太深入的學(xué)習(xí)總結(jié),并且學(xué)習(xí)是一步一步來(lái)的,本文對(duì)JVM的GC做一個(gè)概述,大概知道GC是什么,有哪些GC,參數(shù)有哪些,怎么設(shè)置等等,更為深入具體的對(duì)于GC的學(xué)習(xí)總結(jié)將在未來(lái)持續(xù)輸出。

JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)

下面的圖片展示了JVM的運(yùn)行時(shí)數(shù)據(jù)區(qū):

JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)

程序計(jì)數(shù)器:指示當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)。java虛擬機(jī)的多線程是通過(guò)線程輪流切換并分配處理器執(zhí)行時(shí)間來(lái)實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器都只會(huì)執(zhí)行一條線程中的指令,所以,為了線程切換后能回到正確的位置,每個(gè)線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器,這屬于“線程私有”的內(nèi)存區(qū)間。如果正在執(zhí)行的是一個(gè)java方法,那么pc只指向的就是字節(jié)碼指令的地址,如果正在執(zhí)行的是一個(gè)native方法,那么pc為空(undefined),這個(gè)區(qū)域是java虛擬機(jī)中唯一一個(gè)沒(méi)有規(guī)定OutOfMemoryError情況的區(qū)域。

java虛擬機(jī)棧:屬于線程私有的區(qū)域。虛擬機(jī)棧描述的是java方法執(zhí)行的內(nèi)存模型,每個(gè)方法在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表,操作數(shù)棧,動(dòng)態(tài)鏈接,方法出口等信息,每一個(gè)方法從調(diào)用到執(zhí)行完成的過(guò)程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過(guò)程。局部變量表存放了編譯期間可知的各種基本數(shù)據(jù)類(lèi)型、對(duì)象引用、returnAddress類(lèi)型,局部變量表所需要的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法所需要在棧幀中分配的局部變量表的大小是完全確定的,在方法運(yùn)行期間不會(huì)改變。該區(qū)域會(huì)有兩種類(lèi)型的異常:StackOverFlowError和OutOfMemoryError。如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverFlowError異常,如果虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展,但在擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出OutOfMemoryError異常。

本地方法棧:與虛擬機(jī)棧類(lèi)似,不同點(diǎn)在于虛擬機(jī)棧為虛擬機(jī)執(zhí)行java方法服務(wù),而本地方法棧為虛擬機(jī)使用到的native方法服務(wù)。java堆:java堆是被所有線程共享的一塊大內(nèi)存,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,這個(gè)區(qū)域的唯一目的就是存放對(duì)象實(shí)例:“所有的對(duì)象實(shí)例和數(shù)組都要在堆上分配”。java堆是垃圾回收器主要管理的區(qū)域,所以也稱(chēng)為“GC堆”。java堆可以處于物理上不連續(xù)的空間,只要邏輯上連續(xù)就可以。當(dāng)堆無(wú)法得到擴(kuò)展時(shí),會(huì)拋出OutOfMemoryError異常。

方法區(qū):是各個(gè)線程共享的內(nèi)存區(qū)域,他用于存儲(chǔ)已被虛擬機(jī)加載的類(lèi)信息,常量,靜態(tài)變量,即時(shí)編譯器編譯后產(chǎn)生的代碼等。會(huì)拋出OutOfMemoryError異常。

運(yùn)行時(shí)常量池:這個(gè)方法區(qū)的一部分,class文件中除了有類(lèi)的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池,用于存放邊緣期間產(chǎn)生的各種字面量和符號(hào)引用。java語(yǔ)言并不要求常量一定只有編譯期間產(chǎn)生,運(yùn)行期間也可能將新的常量放入常量池中,比如String的intern方法,該方法確保字符串來(lái)自常量池中,如果發(fā)現(xiàn)常量池中已經(jīng)存在該字符串,則直接返回引用,否則,將這個(gè)字符串加入常量池中,再返回一個(gè)該字符串的引用。

下面的圖片展示了HotSpot JVM的內(nèi)存劃分,本文的內(nèi)容都是基于HotSpot JVM的:

HotSpot JVM內(nèi)存劃分

我們主要關(guān)心的是堆空間,因?yàn)槲覀兊膶?duì)象是分配在堆內(nèi)存上的,可以看出來(lái),在HotSpot JVM的內(nèi)存劃分中,堆被分成了幾個(gè)部分,首先分成了兩個(gè)部分,年輕代和老年代,年輕代又分為了Eden區(qū),from區(qū)和to區(qū),我們的大部分對(duì)象都是分配在Eden區(qū)上的,并且很快就會(huì)被回收,但是有一些對(duì)象總是會(huì)存活下來(lái),當(dāng)滿(mǎn)足一定的條件的時(shí)候,年輕代的某些對(duì)象就會(huì)被移動(dòng)到老年代。發(fā)生在年輕代的垃圾回收活動(dòng)成為Young GC,還有一種垃圾回收活動(dòng)成為Full GC,F(xiàn)ull GC的垃圾收集涉及整個(gè)堆。

HotSpot JVM使用兩種技術(shù)來(lái)加快內(nèi)存分配,分別是”bump-the-pointer“和“TLABs(Thread-Local Allocation Buffers)”。Bump-the-pointer技術(shù)跟蹤在Eden空間創(chuàng)建的最后一個(gè)對(duì)象。這個(gè)對(duì)象會(huì)被放在Eden空間的頂部。如果之后再需要?jiǎng)?chuàng)建對(duì)象,只需要檢查Eden空間是否有足夠的剩余空間。如果有足夠的空間,對(duì)象就會(huì)被創(chuàng)建在Eden空間,并且被放置在頂部。這樣以來(lái),每次創(chuàng)建新的對(duì)象時(shí),只需要檢查最后被創(chuàng)建的對(duì)象。這將極大地加快內(nèi)存分配速度。但是,如果我們?cè)诙嗑€程的情況下,事情將截然不同。如果想要以線程安全的方式以多線程在Eden空間存儲(chǔ)對(duì)象,不可避免的需要加鎖,而這將極大地的影響性能。TLAB(Thread-Local Allocation Buffers) 是HotSpot虛擬機(jī)針對(duì)這一問(wèn)題的解決方案。該方案為每一個(gè)線程在Eden空間分配一塊獨(dú)享的空間,這樣每個(gè)線程只訪問(wèn)他們自己的TLAB空間,再與bump-the-pointer技術(shù)結(jié)合可以在不加鎖的情況下分配內(nèi)存。

初識(shí)GC

首先。GC需要負(fù)責(zé)的工作包括:

分配內(nèi)存

監(jiān)控對(duì)象,如果對(duì)象沒(méi)有引用的時(shí)候需要辨別為垃圾

怎么識(shí)別一個(gè)對(duì)象是垃圾呢?有兩種方法來(lái)分別,一種稱(chēng)為引用計(jì)數(shù)法,另外一種稱(chēng)為可達(dá)性分析。引用計(jì)數(shù)法比較好理解,每個(gè)對(duì)象都有一個(gè)引用計(jì)數(shù)器,當(dāng)有一個(gè)地方引用了該對(duì)象,這個(gè)計(jì)數(shù)器就加1,當(dāng)引用失效的時(shí)候減1,任何時(shí)候只要這個(gè)引用值為0的時(shí)候就成垃圾了(無(wú)法解決循環(huán)引用的問(wèn)題)。另外一種辨別垃圾對(duì)象的算法為可達(dá)性分析,這個(gè)算法的基本思路是通過(guò)一系列名為 “GC Roots” 的對(duì)象作為起始點(diǎn),從這些根節(jié)點(diǎn)開(kāi)始向下搜索,搜索所走過(guò)的路徑稱(chēng)為引用鏈(Reference Chain),當(dāng)一個(gè)對(duì)象到 GC Roots 沒(méi)有任何引用鏈相連時(shí),則證明此對(duì)象是不可用的,就可以納入可回收的范圍。在 Java 語(yǔ)言里,可作為 GC Roots 對(duì)象的包括如下幾種:

虛擬機(jī)棧(棧楨中的本地變量表)中的引用的對(duì)象;

方法區(qū)中的類(lèi)靜態(tài)屬性引用的對(duì)象;

方法區(qū)中的常量引用的對(duì)象;

本地方法棧中 JNI(一般說(shuō)的Native方法) 的引用的對(duì)象。

GC必須是安全可靠的,存活的對(duì)象不能被錯(cuò)誤的標(biāo)記為垃圾而被釋放掉,而不再存活的對(duì)象應(yīng)該被及時(shí)標(biāo)記并且被回收。GC的執(zhí)行應(yīng)該是高效的,因?yàn)橐话銇?lái)說(shuō),GC在進(jìn)行垃圾收集的時(shí)候需要Stop-the-world,所以,GC活動(dòng)的時(shí)間越短越好,還有一點(diǎn)需要注意的是,GC過(guò)后可能會(huì)造成內(nèi)存碎片問(wèn)題,為了清除內(nèi)存碎片,應(yīng)該選擇具備內(nèi)存壓縮能力的GC算法,但是這個(gè)過(guò)程需要非常高效,因?yàn)檫@不是GC的主要功能,合理的做法應(yīng)該是GC之后不強(qiáng)制進(jìn)行內(nèi)存壓縮操作,只有在GC過(guò)后一段時(shí)間頻繁發(fā)現(xiàn)因?yàn)閮?nèi)存碎片問(wèn)題而造成內(nèi)存申請(qǐng)失敗的情況下再進(jìn)行內(nèi)存壓縮,因?yàn)檫@個(gè)時(shí)候JVM除了內(nèi)存壓縮別無(wú)選擇。下面列舉出了幾個(gè)設(shè)計(jì)GC的性能指標(biāo):

吞吐量:垃圾回收的過(guò)程應(yīng)該盡量高效,運(yùn)行正常的應(yīng)用的時(shí)間占比應(yīng)該盡量高

垃圾收集代價(jià):垃圾收集所占的時(shí)間應(yīng)該盡量少

Stop-the-world的時(shí)候應(yīng)該盡量少

垃圾收集頻率:不能頻繁進(jìn)行GC活動(dòng),時(shí)間應(yīng)該花在應(yīng)用的運(yùn)行上

GC算法分類(lèi)

GC算法可以分為下面幾類(lèi):

標(biāo)記-清除算法(Mark-Sweep):首先標(biāo)記出所有需要清除的對(duì)象,然后進(jìn)行清除。缺點(diǎn)有兩方面,在效率上來(lái)說(shuō),標(biāo)記和清除的過(guò)程效率都不高,在空間上來(lái)說(shuō),清除過(guò)后會(huì)產(chǎn)生很多不連續(xù)的內(nèi)存碎片,會(huì)造成很多內(nèi)存碎片問(wèn)題。

復(fù)制算法(Copying):算法將可用內(nèi)存分為相同的兩部分,每次只使用其中的一塊,當(dāng)這一塊內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已經(jīng)使用過(guò)的內(nèi)存一次性清理掉。在對(duì)象存活率較高的時(shí)候效率會(huì)較低,因?yàn)橐M(jìn)行較多的復(fù)制操作。

標(biāo)記-整理算法(Mark-Compact):和標(biāo)記-清除一樣,前一個(gè)過(guò)程是一樣的,都是將可回收的對(duì)象標(biāo)記起來(lái),但是標(biāo)記-整理算法在標(biāo)記起來(lái)之后,不是進(jìn)行簡(jiǎn)單的清除,而是將所有的存活對(duì)象往一端移動(dòng),然后將那些需要清除的對(duì)象清除。

分代回收算法(Generational GC):當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用“分代收集”算法。就是講java堆分為新生代和老年代,新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大量的對(duì)象死去,只有少量存活,就選用復(fù)制算法;而在老年代中,對(duì)象存活率很高,就使用“標(biāo)記-清除”或者“標(biāo)記-整理”算法。

現(xiàn)代JVM的GC大多都是基于分代回收的,所謂分代,就是將堆分成不同的代,然后對(duì)不同的代進(jìn)行不同的回收。在HotSpot JVM的實(shí)現(xiàn)中,GC被分為兩類(lèi):

Partial GC:并不會(huì)收集整個(gè)堆

(1)、Young GC:只會(huì)收集Young Gen的GC算法

(2)、Old GC:只會(huì)收集Old Gen的GC(只有CMS的concurrent collection是這個(gè)模式)

(3)、Mixed GC:收集整個(gè)young gen以及部分old gen的GC。只有G1有這個(gè)模式

Full GC:收集整個(gè)堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。

可以根據(jù)GC算法的運(yùn)行模式分為下面幾類(lèi):

單線程GC:?jiǎn)尉€程GC在實(shí)現(xiàn)上使用單一線程來(lái)進(jìn)行垃圾收集活動(dòng),比如Serial GC

并行GC:每次運(yùn)行時(shí),不管是YGC,還是FGC,會(huì) stop-the-world,暫停所有的用戶(hù)線程,并采用多個(gè)線程同時(shí)進(jìn)行垃圾收集,比如Parallel GC。

并發(fā)GC:在新生代進(jìn)行垃圾收集時(shí)和并行收集器類(lèi)似,都是并行收集,而且都會(huì)stop-the-world,主要的區(qū)別在于老年代的收集上,在老年代進(jìn)行垃圾收集時(shí),大部分時(shí)間可以和用戶(hù)線程并發(fā)執(zhí)行的,只有小部分的時(shí)間stop-the-world,這就是它的優(yōu)勢(shì),可以大大降低應(yīng)用的暫停時(shí)間,比如CMS GC。

Serial GC

Serial GC屬于單線程的GC,在進(jìn)行垃圾收集的時(shí)候需要Stop-the-world直到Serial GC工作完成。使用串行收集器的年輕代垃圾收集。Eden區(qū)的活躍對(duì)象(live狀態(tài)的對(duì)象)會(huì)被拷貝到初始為空的Survivor區(qū)中,這其中,那些體積過(guò)大以至于Survivor區(qū)裝不下的對(duì)象不會(huì)進(jìn)行拷貝。這些對(duì)象會(huì)被拷貝到老年代中。相對(duì)于已經(jīng)被拷貝到To區(qū)的對(duì)象,源Survivor區(qū)中的live對(duì)象仍然比較年輕,而被拷貝到老年代中對(duì)象則相對(duì)年紀(jì)大一些。注意,若To區(qū)已經(jīng)滿(mǎn)了,來(lái)自Eden區(qū)或From區(qū)的對(duì)象就無(wú)法被拷貝到To區(qū)了,那么這些對(duì)象會(huì)被調(diào)整,無(wú)論經(jīng)過(guò)多少次年輕代的垃圾收集,這些對(duì)象都不會(huì)被釋放掉。在live對(duì)象被拷貝之后,Eden區(qū)和From區(qū)中還存在的對(duì)象就不再是live的了,它們不會(huì)再被檢測(cè)。在年輕代垃圾收集完成后,Eden區(qū)和From區(qū)會(huì)被清空,只有To區(qū)會(huì)繼續(xù)持有l(wèi)ive狀態(tài)的對(duì)象。此時(shí),F(xiàn)rom區(qū)和To區(qū)在邏輯上交換,To區(qū)變成From區(qū),原From區(qū)變成To區(qū)。

對(duì)于串行收集器,老年代和永生代會(huì)在進(jìn)行垃圾收集時(shí)使用標(biāo)記-清理-壓縮(Mark-Sweep-Compact)算法。在標(biāo)記階段,收集器會(huì)標(biāo)識(shí)哪些對(duì)象是live狀態(tài)的。清理階段會(huì)跨代清理,標(biāo)識(shí)垃圾對(duì)象。然后,收集器執(zhí)行移動(dòng)壓縮(sliding compaction),將live對(duì)象移動(dòng)到老年代內(nèi)存空間的起始部分(永生代中情況于此類(lèi)似),這樣在老年代內(nèi)存空間的尾部會(huì)產(chǎn)生一個(gè)大的連續(xù)空間。

在非服務(wù)器類(lèi)使用的機(jī)器上,默認(rèn)選擇的是串行垃圾收集器。在其他類(lèi)型使用的機(jī)器上,可以通過(guò)添加參數(shù)-XX:+UseSerialGC來(lái)顯式的使用串行垃圾收集器。

Serial Old GC

Serial Old GC是Serial GC的老年代版本,它同樣是一個(gè)單線程GC,使用”標(biāo)記-整理“算法,這個(gè)GC的主要意義在于給Client模式下的JVM使用,如果是在Server模式下,它還有兩個(gè)用途,一種用在JDK 1.5以及之前的版本中與Parallel Scavenge GC搭配使用,另外一種用途是作為CMS GC的后備預(yù)案GC,在并發(fā)收集發(fā)生Concurrent Mode Failure的時(shí)候使用。

ParNew GC

ParNew GC是Serial GC的多多線程版本,除了使用多線程進(jìn)行垃圾收集之外,其余的行為都和Serial GC一致。

Parallel Scavenge GC

Parallel Scavenge GC是用于新生代的一種GC,它屬于并行的多線程收集器,使用復(fù)制算法,和ParNew GC類(lèi)似。Parallel Scavenge GC的特別之處在于,它關(guān)注的是如何達(dá)到一個(gè)可控的GC吞吐量,而其他比如CMS等GC關(guān)注的則是盡量縮短GC導(dǎo)致的用戶(hù)線程的停頓時(shí)間。

Parallel Old GC

Parallel Old GC是Parallel Scavenge GC的老年代版本,使用多線程和”標(biāo)記-整理“算法,在比較注重吞吐量的場(chǎng)景下可以使用Parallel Scavenge GC + Parallel Old GC的GC組合,年輕代使用Parallel Scavenge GC,老年代則使用 Parallel Old GC。下面的圖片展示了Serial GC和Parallel GC的區(qū)別:

CMS GC

下面的圖片首先展示了Serial GC 和CMS GC的區(qū)別:

下面的圖片展示了CMS中的內(nèi)存結(jié)構(gòu):

CMS內(nèi)存結(jié)構(gòu)

CMS GC的執(zhí)行流程如下:

第一步初始化標(biāo)記(initial mark):這一步驟只是查找那些距離類(lèi)加載器最近的幸存對(duì)象。因此,停頓的時(shí)間非常短暫。

第二步并行標(biāo)記( concurrent mark ):所有被幸存對(duì)象引用的對(duì)象會(huì)被確認(rèn)是否已經(jīng)被追蹤和校驗(yàn)。這一步的不同之處在于,在標(biāo)記的過(guò)程中,其他的線程依然在執(zhí)行。

第三步重新標(biāo)記(remark):會(huì)再次檢查那些在并行標(biāo)記步驟中增加或者刪除的與幸存對(duì)象引用的對(duì)象。

第四步并發(fā)清除( concurrent sweep ):轉(zhuǎn)交垃圾回收過(guò)程處理。垃圾回收工作會(huì)在其他線程的執(zhí)行過(guò)程中展開(kāi)。

一旦采取了這種GC類(lèi)型,由GC導(dǎo)致的暫停時(shí)間會(huì)極其短暫。CMS GC也被稱(chēng)為低延遲GC。它經(jīng)常被用在那些對(duì)于響應(yīng)時(shí)間要求十分苛刻的應(yīng)用之上。當(dāng)然,這種GC類(lèi)型在擁有stop-the-world時(shí)間很短的優(yōu)點(diǎn)的同時(shí),也有如下缺點(diǎn):

它會(huì)比其他GC類(lèi)型占用更多的內(nèi)存和CPU

默認(rèn)情況下不支持壓縮步驟

在使用這個(gè)GC類(lèi)型之前你需要慎重考慮。如果因?yàn)閮?nèi)存碎片過(guò)多而導(dǎo)致壓縮任務(wù)不得不執(zhí)行,那么stop-the-world的時(shí)間要比其他任何GC類(lèi)型都長(zhǎng),你需要考慮壓縮任務(wù)的發(fā)生頻率以及執(zhí)行時(shí)間。

G1 GC

G1 GC是目前為止最復(fù)雜、也是最先進(jìn)的GC,在CMS 算法中,GC 管理的內(nèi)存被劃分為新生代、老年代和永久代/元空間。這些空間必須是地址連續(xù)的。在G1算法中,采用了另外一種完全不同的方式組織堆內(nèi)存,堆內(nèi)存被劃分為多個(gè)大小相等的內(nèi)存塊(Region),每個(gè)Region是邏輯連續(xù)的一段內(nèi)存,Region的大小可以通過(guò) -XX:G1HeapRegionSize 參數(shù)指定,如果沒(méi)有設(shè)置,默認(rèn)把堆內(nèi)存按照2048份均分,最后得到一個(gè)合理的大小。在G1中,還有一種特殊的區(qū)域,叫 Humongous 區(qū)域。 如果一個(gè)對(duì)象占用的空間超過(guò)了分區(qū)容量 50% 以上,G1 收集器就認(rèn)為這是一個(gè)巨型對(duì)象。這些巨型對(duì)象,默認(rèn)直接會(huì)被分配在年老代,但是如果它是一個(gè)短期存在的巨型對(duì)象,就會(huì)對(duì)垃圾收集器造成負(fù)面影響。為了解決這個(gè)問(wèn)題,G1 劃分了一個(gè) Humongous 區(qū),它用來(lái)專(zhuān)門(mén)存放巨型對(duì)象。下面的圖片展示了G1的內(nèi)存結(jié)構(gòu):

G1 GC內(nèi)存結(jié)構(gòu)

G1 GC的運(yùn)行可以分為下面幾個(gè)階段:

初始標(biāo)記(STW initial marking):掃描根集合,標(biāo)記所有從根集合可直接到達(dá)的對(duì)象并將它們的字段壓入掃描棧。在分代式G1模式中,初始標(biāo)記階段借用 Young GC 的暫停,因而沒(méi)有額外的、單獨(dú)的暫停階段。

并發(fā)標(biāo)記(concrrent marking):這個(gè)階段可以并發(fā)執(zhí)行,GC 線程 不斷從掃描棧取出引用,進(jìn)行遞歸標(biāo)記,直到掃描棧清空。

最終標(biāo)記(STW final marking,在實(shí)現(xiàn)中也叫Remarking):重新標(biāo)記寫(xiě)入屏障( Write Barrier)標(biāo)記的對(duì)象,這個(gè)階段也進(jìn)行弱引用處理(reference processing)。

篩選回收(Live Data Counting And evacuation):統(tǒng)計(jì)每個(gè) Region 被標(biāo)記為活的對(duì)象有多少,如果發(fā)現(xiàn)完全沒(méi)有活對(duì)象的 Region 就會(huì)將其整體回收到可分配 Region 列表中。

與其他GC相比,G1 GC有如下特點(diǎn):

并行與并發(fā):G1 GC能充分利用CPU、多核心等硬件優(yōu)勢(shì),使用多個(gè)CPU或者CPU核心來(lái)縮短STW的時(shí)間,部分其他GC需要停頓java線程執(zhí)行的GC操作,在G1 GC中任然可以通過(guò)并發(fā)的方式讓java程序繼承執(zhí)行。

分代收集:和其他GC一樣,分代的概念在G1 GC中任然保留。

空間整合:與CMS的標(biāo)記-清理算法不同,G1 GC從整體來(lái)看是通過(guò)”標(biāo)記-整理“算法實(shí)現(xiàn)的GC,從局部(兩個(gè)Region之間)來(lái)看是通過(guò)”復(fù)制“算法來(lái)實(shí)現(xiàn)的,無(wú)論如何,這兩種算法在運(yùn)行期間都不會(huì)產(chǎn)生內(nèi)存碎片,GC 活動(dòng)之后可以提供規(guī)整的內(nèi)存空間。

可預(yù)測(cè)的停頓:這是G1 GC相對(duì)于CMS的另一大優(yōu)勢(shì),降低停頓時(shí)間是G1 GC和CMS GC共同關(guān)注的,但是G1 GC除了追求低停頓時(shí)間外,還建立了可預(yù)測(cè)的停頓時(shí)間模型,能讓使用這明確指定在一個(gè)長(zhǎng)度為M的時(shí)間片內(nèi),消耗在垃圾收集上的時(shí)間不得超過(guò)N毫秒。

下面的圖片展示了多個(gè)GC以及他們工作的分代位置,以及如何組合使用:

多種GC組合模式

JVM GC的觸發(fā)條件

上文中提到了很多的GC,但是沒(méi)有提到會(huì)在什么時(shí)候觸發(fā)這些GC開(kāi)始工作,根據(jù)HotSpot JVM的Serial GC的實(shí)現(xiàn)來(lái)看,觸發(fā)GC工作的條件如下:

Young GC:當(dāng)Young generation中的Eden區(qū)分配滿(mǎn)的時(shí)候觸發(fā)。

Full GC:(1)、當(dāng)準(zhǔn)備要觸發(fā)一次young GC時(shí),如果發(fā)現(xiàn)統(tǒng)計(jì)數(shù)據(jù)Young GC的平均晉升大小比目前old gen剩余的空間大,則不會(huì)觸發(fā)Young GC而是轉(zhuǎn)為觸發(fā)Full GC。(2)、如果有perm gen的話,要在perm gen分配空間但已經(jīng)沒(méi)有足夠空間時(shí),也要觸發(fā)一次Full GC(3)、調(diào)用System.gc()默認(rèn)也是觸發(fā)Full GC。

每個(gè)GC觸發(fā)的條件都應(yīng)該不太一樣但是整體上是一樣的,對(duì)于每一個(gè)GC的觸發(fā)條件,需要研究每一個(gè)GC的實(shí)現(xiàn),這些內(nèi)容將在未來(lái)合適的時(shí)候補(bǔ)充上,本文點(diǎn)到為止。

GC參數(shù)說(shuō)明

下面的表格說(shuō)明了JVM的GC參數(shù)設(shè)置細(xì)節(jié):

JVM參數(shù)描述

UseSerialGC虛擬機(jī)運(yùn)行在Client模式下的默認(rèn)值,打開(kāi)此開(kāi)關(guān)后,采用Serial + Serial Old的GC組合進(jìn)行內(nèi)存回收

UseParNewGC打開(kāi)此開(kāi)關(guān)后,使用ParNew + Serial Old的GC組合二

UseConcMarkSweepGC使用ParNew + CMS + Serial Old 的GC組合,Serial Old GC作為CMS GC出現(xiàn)Concurrent Mode Failure后的備用GC

UseParallelGCJVM 運(yùn)行在Server模式下的默認(rèn)值,使用Parallel Scavenge GC + Serial Old 的GC組合

UseParallelOldGC使用Parallel Scavenge GC + Parallel Old GC的GC組合

UseG1GC使用G1 GC來(lái)進(jìn)行垃圾收集

MaxGCPauseMillis設(shè)置期望的GC停頓時(shí)間,僅對(duì)G1 GC有用

SurvivorRatio新生代中Eden區(qū)域與Survivor區(qū)域的容量比值,默認(rèn)為8,代表Eden : Survivor? = 8 : 1

PretenureSizeThreshold直接晉升到老年代的對(duì)象大小閾值,設(shè)置這個(gè)參數(shù)后,大于這個(gè)參數(shù)的對(duì)象將直接晉升為老年代

MaxTenuringThreshold晉升到老年代的對(duì)象年齡,每個(gè)對(duì)象在堅(jiān)持一次Young GC之后年齡增加1,超過(guò)這個(gè)參數(shù)就會(huì)晉升到老年代

UseAdaptiveSizePolicy動(dòng)態(tài)調(diào)整java堆中各個(gè)區(qū)域的大小以及進(jìn)入老年代的年齡

ParallelGCThreads設(shè)置并行GC時(shí)進(jìn)行內(nèi)存回收的線程數(shù)量

GCTimeRatioGC時(shí)間占總時(shí)間的比率,默認(rèn)為1%,僅在使用Parallel Scavenge GC的時(shí)候有效

MaxGCPauseMillis設(shè)置GC的最大停頓時(shí)間,僅對(duì)Parallel Scavenge GC有效

CMSInitiatingOccupancyFraction設(shè)置CMS GC在老年代空間被使用多少后出發(fā)GC默認(rèn)值為68%,僅對(duì)于CMS有效

UseCMSCompactAtFullCollection設(shè)置CMS GC在進(jìn)行一次垃圾收集之后是否需要進(jìn)行內(nèi)存碎片整理

CMSFullGCsBeforeCompaction設(shè)置CMS GC在進(jìn)行了若干次垃圾收集之后進(jìn)行一次內(nèi)存碎片整理

理解GC日志

在設(shè)置JVM參數(shù)的時(shí)候,可以設(shè)置GC打印日志參數(shù):-XX:+PrintGCDetails。下面是兩條典型的GC輸出日志:

33.125: [GC [DefNew:3324k->152k(3712k),0.0025925secs]3324k->152k(11904k),0.0031680secs]100.667: [Full GC [Tenured:0k->210k(10240k),0.0149142secs]4603k->210k(19456k), [Perm:2999k->2999k(21248k)],0.0150007secs] [Times: user=0.01sys=0.00real=0.02secs]

下面來(lái)解析一下GC日志,每個(gè)輸出字段代表什么意思。首先是最前面的33.125和100.667,代表的是GC發(fā)生的時(shí)間,這個(gè)數(shù)字的含義是從Java JVM啟動(dòng)以來(lái)經(jīng)過(guò)的秒數(shù)。然后是日志開(kāi)頭的“[GC” 和“[Full GC”說(shuō)明了這次垃圾收集的類(lèi)型。接下來(lái)的“[DefNew”、"[Tenured"、“[Perm”表示GC發(fā)生的區(qū)域,而后面的"3324k->152k(3712k)"表示的是“GC前該區(qū)域已使用量->GC后該區(qū)域已使用量(該區(qū)域總量)”。而在后面的"3324k->152k(11904k)"表示的是“GC前Java堆已使用量->GC后Java堆的使用量(Java堆的總量)”。后面的0.0025925 secs等表示的是該區(qū)域GC的時(shí)間。而[Times: user=0.01 sys=0.00 real=0.02 secs]則是更為詳細(xì)的時(shí)間占比統(tǒng)計(jì)。在多核心或者多CPU以及使用多線程的情況下,多線程操作會(huì)疊加這些時(shí)間,所以u(píng)ser、sys以及real之間并沒(méi)有某種恒等關(guān)系。

JVM 性能監(jiān)控與故障處理工具

本節(jié)列出幾個(gè)用于JVM性能監(jiān)控與故障處理的有用工具,具體的使用細(xì)節(jié)還需要分別學(xué)習(xí),并且在平時(shí)的實(shí)戰(zhàn)中積累經(jīng)驗(yàn)。

jps : JVM進(jìn)程狀況工具

jps命令格式:jps [options] [hostid]

jps主要選項(xiàng):

選項(xiàng)作用

-m輸出JVM進(jìn)程啟動(dòng)時(shí)傳遞給主類(lèi)main方法的參數(shù)

-l輸出主類(lèi)的全名,如果進(jìn)程執(zhí)行的是jar包,輸出jar包的路徑

-v輸出進(jìn)程啟動(dòng)時(shí)的JVM參數(shù)

jstat:JVM 統(tǒng)計(jì)信息監(jiān)控工具

該工具具有豐富的JVM統(tǒng)計(jì)功能,具體支持的統(tǒng)計(jì)可以使用man jstat來(lái)輸出幫助文檔,下面以一個(gè)使用例子來(lái)說(shuō)明該工具的使用方法:

首先運(yùn)行一個(gè)Java程序,然后使用jps獲取這個(gè)java進(jìn)程的進(jìn)程id,然后使用命令jstat命令來(lái)查看JVM的統(tǒng)計(jì)信息,比如我在本機(jī)啟動(dòng)了一個(gè)java進(jìn)程,id為61659,然后使用jstat命令:jstat -gc 61659 1000 5,則輸出內(nèi)容如下:

S0C? ? S1C? ? S0U? ? S1U? ? ? EC? ? ? EU? ? ? ? OC? ? ? ? OU? ? ? MC? ? MU? ? CCSC? CCSU? YGC? ? YGCT? ? FGC? ? FGCT? ? GCT10752.010752.00.00.065536.01310.82796544.01085.14992.04419.0640.0504.430.00420.0180.02210752.010752.00.00.065536.01310.82796544.01085.14992.04419.0640.0504.430.00420.0180.02210752.010752.00.00.065536.01310.82796544.01085.14992.04419.0640.0504.430.00420.0180.02210752.010752.00.00.065536.01310.82796544.01085.14992.04419.0640.0504.430.00420.0180.02210752.010752.00.00.065536.01310.82796544.01085.14992.04419.0640.0504.430.00420.0180.022

我使用了jstat的gc統(tǒng)計(jì)選項(xiàng),并且輸出了所有的統(tǒng)計(jì)項(xiàng)信息,關(guān)于每個(gè)輸出列的含義,可以參考下面的說(shuō)明:

+-------+-------------------------------------------+? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |Column |? ? ? ? ? ? ? ? Description? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? +-------+-------------------------------------------+? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |SOC? ? | Current survivor space0capacity (KB).? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |S1C? ? | Current survivor space1capacity (KB).? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |S0U? ? | Survivor space0utilization (KB).? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |S1U? ? | Survivor space1utilization (KB).? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |EC? ? |Current eden spacecapacity(KB).? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |EU? ? | Eden spaceutilization(KB).? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |OC? ? | Current old spacecapacity(KB).? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |OU? ? | Old spaceutilization(KB).? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |PC? ? | Current permanent spacecapacity(KB).? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |PU? ? | Permanent spaceutilization(KB).? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |YGC? ? | Number of young generation GC Events.? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |YGCT? | Young generation garbage collection time. |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |FGC? ? | Number of full GC events.? ? ? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |FGCT? | Full garbage collection time.? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? |GCT? ? | Total garbage collection time.? ? ? ? ? ? |? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? +-------+-------------------------------------------+

jinfo:java配置信息工具

jinfo用于獲取當(dāng)前JVM的配置信息,比如下面的命令:

jinfo -flag PrintGCDetails61659

輸出內(nèi)容為:

-XX:-PrintGCDetails

jmap:java內(nèi)存映射工具

jmap用于生成堆的轉(zhuǎn)儲(chǔ)快照,下面為一個(gè)使用示例,用于將當(dāng)前的JVM的堆的快照輸出到文件中去:

jmap -dump:format=b,file=heapdump.data pid

jhat:JVM堆轉(zhuǎn)儲(chǔ)快照分析工具

配合jmap使用,jmap用于生成堆的轉(zhuǎn)儲(chǔ)快照,而jhat可以將jmap的輸出可視化,比如對(duì)于上面的輸出文件heapdump.data文件,可以使用下面的命令生成可視化html:

jhat heapdump.data

等jhat執(zhí)行完畢后,就可以打開(kāi)瀏覽器查看堆的情況的。

jstack:JVM堆棧追蹤工具

jstack用于生成當(dāng)前堆棧的線程快照,這個(gè)命令會(huì)將所有在堆上的線程都輸出,包括線程的運(yùn)行狀態(tài),持有資源的狀態(tài)等等,對(duì)于java應(yīng)用調(diào)優(yōu),jstack是非常有用的。比如在我的機(jī)器上執(zhí)行下面的命令:

jstack61659> threaddump.data

那么java進(jìn)程id為61659的進(jìn)程的堆棧的線程信息會(huì)被輸出到文件threaddump.data文件中去,下面是一小段內(nèi)容:

"pool-1-thread-1"#11prio=5os_prio=31tid=0x00007f9bad840800nid=0x5a03waiting on condition [0x000070000d8fa000]? java.lang.Thread.State: WAITING (parking)? ? at sun.misc.Unsafe.park(Native Method)? ? - parking to waitfor<0x00000006c006ab48> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)? ? at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)? ? at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)? ? at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081)? ? at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809)? ? at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)? ? at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)? ? at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)? ? at java.lang.Thread.run(Thread.java:748)

這是線程名字為pool-1-thread-1的線程的堆棧信息,可以看出它目前的狀態(tài)是WAITING (parking),并且可以看出是因?yàn)閜arking to wait for? <0x00000006c006ab48> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject),線程id的16進(jìn)制表示為0x5a03,所以在需要查看某個(gè)線程的堆棧狀態(tài)的時(shí)候需要先查找到線程id,然后將這個(gè)線程id轉(zhuǎn)換為16進(jìn)制(printf "%x\n" id),然后使用jstack將當(dāng)前的堆棧內(nèi)的線程快照輸出到文件,然后在這個(gè)文件中查找相應(yīng)的線程來(lái)看起狀態(tài),看是否在等待鎖等信息。

結(jié)語(yǔ)

本文在一個(gè)較牽線的層面上進(jìn)行了一些關(guān)于java JVM GC的總結(jié)記錄,主要目的是希望自己對(duì)GC以及JVM有一些大概的認(rèn)識(shí),并且希望自己能在未來(lái)的很長(zhǎng)一段時(shí)間持續(xù)學(xué)習(xí)關(guān)于JVM以及GC的內(nèi)容,并且能夠有一些總結(jié)文檔輸出。文章中的大部分內(nèi)容都出自《深入理解Java虛擬機(jī)(第二版)》一書(shū),該書(shū)是關(guān)于JVM的經(jīng)典書(shū)籍,需要多次研讀,文章中還有很多內(nèi)容來(lái)自各種資料,目前流傳的關(guān)于JVM的資料很多,內(nèi)容魚(yú)龍混雜,所以本文可能會(huì)漏洞百出,并且相等不全面,但是這些都不是問(wèn)題,有問(wèn)題的內(nèi)容將在未來(lái)的某個(gè)時(shí)候被修復(fù),更為具體、深入、全面的關(guān)于JVM以及GC的內(nèi)容將在未來(lái)會(huì)不斷總結(jié)輸出,學(xué)無(wú)止境?。?/p>

作者:一字馬胡

鏈接:http://www.lxweimin.com/p/2a8d6231d995

來(lái)源:簡(jiǎn)書(shū)

著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,011評(píng)論 3 413
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 175,263評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 62,543評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,323評(píng)論 6 404
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 54,874評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評(píng)論 3 439
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,095評(píng)論 0 286
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,605評(píng)論 1 331
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,551評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,720評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評(píng)論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,961評(píng)論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,358評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,612評(píng)論 1 280
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,330評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,690評(píng)論 2 370

推薦閱讀更多精彩內(nèi)容