理解jvm(二)--垃圾回收

jvm經(jīng)過多年的發(fā)展,它的垃圾回收算法也經(jīng)過了幾個迭代。本文主要介紹垃圾回收算法及目前hotspot上實現(xiàn)的垃圾回收器,以及他們的優(yōu)缺點。

1、對象分配

? ? java引以為傲的一點是java內(nèi)存的管理完全由jvm管理,程序員不再需要顯式的分配和回收內(nèi)存。這樣做的好處是顯而易見的,提高了程序員們的效率,也減少了由于程序錯誤造成的內(nèi)存溢出錯誤。當(dāng)然這樣做也有缺點,比如做相同的事它更占內(nèi)存。消耗更多的資源。但是在入境人比機(jī)器貴的環(huán)境下,java顯然更迎合時代的潮流,所有java才連續(xù)多年稱為熱門的計算機(jī)程序語言之一。jvm在垃圾回收的時候會有短時間的程序停擺(所有線程中斷執(zhí)行,進(jìn)行垃圾回收)。java構(gòu)造對象只需要一個new 關(guān)鍵字。

? ? 我們知道我們在new一個對象時,java虛擬機(jī)會給這個對象分配內(nèi)存,那么分配在哪里呢?新生對象一般會被分配在新生代的eden區(qū)。除非這個對象足夠大,對于大對象jvm有可能會將它分配到老年代,這樣可以避免大對象占滿新生代,從而引發(fā)gc,因為新生代的區(qū)域相對較小,且eden只占其中的一部分(默認(rèn)是eden和一個survivor比例餓為8:1,可通過-XX:SurvivorRatio參數(shù)設(shè)置。)。也可通過參數(shù)-XX:PretenureSizeThreshold來設(shè)置超過多大的對象在老年代分配。

2.如何判定對象的生死?

? ??判定對象是不是需要回收主要有兩種方法,

? ? 一種是引用計數(shù)法:為對象增加一個引用計數(shù)器,當(dāng)一個地方引用了該對象時,計數(shù)器加1,當(dāng)引用失效時,計數(shù)器減1,當(dāng)引用計數(shù)器為0時,表明該對象永遠(yuǎn)不會被再使用,可以回收。但是HotSpot(其實絕大部分的虛擬機(jī)實現(xiàn)都不是這種方法)不是使用引用計數(shù)算法,因為解決不掉對象之間相互引用的情況。

? ? 一種是根搜索算法:基本思想是通過一條“GC Roots”的對象座位起點,從這個節(jié)點向下搜索,搜索所經(jīng)過的的路徑稱為引用鏈(Reference Chain),當(dāng)一個對象到GC Roots沒有任何引用鏈鏈接時,則證明此對象不可達(dá)。是可以被回收的對象。但不可達(dá)的對象是非死不可的嗎?其實不是,不可達(dá)的對象如果重寫了finalize方法且沒有被系統(tǒng)調(diào)用過(一個對象的finalize方法最多被調(diào)用一次),對象被第一次標(biāo)記并進(jìn)入F-QUEUE隊列等待被執(zhí)行finalize方法,如果finalize方法里重新與CG root建立可達(dá)性連接則此對象就不被回收,若沒有建立可達(dá)性連接則第二次被標(biāo)記,第二次被標(biāo)記的對象就會被回收。若不可達(dá)對象沒有重寫finalize方法,則被兩次標(biāo)記后被回收。這種也是主流jvm垃圾回收使用的算法。

? ? 方法區(qū)的垃圾回收:方法區(qū)的垃圾回收主要包含兩部分,1.無用的類,2.廢棄常量。那么哪些是無用的類,哪些是廢棄常量呢。何判斷廢棄常量呢?以字面量回收為例,如果一個字符串“abc”已經(jīng)進(jìn)入常量池,但是當(dāng)前系統(tǒng)沒有任何一個String對象引用了叫做“abc”的字面量,那么,如果發(fā)生垃圾回收并且有必要時,“abc”就會被系統(tǒng)移出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。如何判斷無用的類呢?需要滿足以下三個條件1. 該類的所有實例都已經(jīng)被回收,即Java堆中不存在該類的任何實例。2. 加載該類的ClassLoader已經(jīng)被回收。3. 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

3.垃圾回收算法

1.標(biāo)記-清除算法(Mark-Sweep),首先標(biāo)識出需要回收的對象,然后統(tǒng)一進(jìn)行回收,這種算法效率不高,而且會產(chǎn)生大量的內(nèi)存碎片,現(xiàn)在并不被主流垃圾回收器使用。

2.復(fù)制算法,它將可用的內(nèi)存分為兩塊,每次只使用其中一塊,每次其中一塊快使用完時,就將這塊內(nèi)存中還存活的對象復(fù)制到另外一塊內(nèi)存中,另外一塊內(nèi)存區(qū)域集中清理,雖然這樣做不會產(chǎn)生內(nèi)存碎片,但是這樣做的代價也是高昂的,因為可用內(nèi)存只有真正內(nèi)存的一半,所以這種算法只用來回收新生代 垃圾,新生代也不是分為兩個區(qū)域,而是分為eden區(qū),和兩個Sruvivor區(qū)。?當(dāng)新生代發(fā)生每次回收時,將Eden和Survivor中還存活著的對象一次性復(fù)制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機(jī)默認(rèn)Eden區(qū)和Survivor區(qū)的比例為8:1,意思是每次新生代中可用內(nèi)存空間為整個新生代容量的90%。當(dāng)然,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當(dāng)Survivor空間不夠用時,需要依賴?yán)夏甏M(jìn)行分配擔(dān)保(Handle Promotion)。

3.標(biāo)記-整理(Mark-Compact)算法,根據(jù)老年代的特點,有人提出了另外一種標(biāo)記-整理算法,過程與標(biāo)記-清除算法一樣,不過不是直接對可回收對象進(jìn)行清理,而是讓所有存活對象都向一端移動,然后直接清理掉邊界以外的內(nèi)存?,F(xiàn)代商用虛擬機(jī)基本都采用分代收集算法來進(jìn)行垃圾回收。這種算法沒什么特別的,無非是上面內(nèi)容的結(jié)合罷了,根據(jù)對象的生命周期的不同將內(nèi)存劃分為幾塊,然后根據(jù)各塊的特點采用最適當(dāng)?shù)氖占惴?。大批對象死去、少量對象存活的(新生代),使用?fù)制算法,復(fù)制成本低;對象存活率高、沒有額外空間進(jìn)行分配擔(dān)保的(老年代),采用標(biāo)記-清理算法或者標(biāo)記-整理算法。

4.垃圾收集器

垃圾收集器就是上面講的理論知識的具體實現(xiàn)了。不同虛擬機(jī)所提供的垃圾收集器可能會有很大差別,我們使用的是HotSpot.

Serial收集器

Serial收集器是最古老的收集器,它的缺點是當(dāng)Serial收集器想進(jìn)行垃圾回收的時候,必須暫停用戶的所有進(jìn)程,即stop the world。到現(xiàn)在為止,它依然是虛擬機(jī)運(yùn)行在client模式下的默認(rèn)新生代收集器,與其他收集器相比,對于限定在單個CPU的運(yùn)行環(huán)境來說,Serial收集器由于沒有線程交互的開銷,專心做垃圾回收自然可以獲得最高的單線程收集效率。

Serial Old是Serial收集器的老年代版本,它同樣是一個單線程收集器,使用”標(biāo)記-整理“算法。這個收集器的主要意義也是被Client模式下的虛擬機(jī)使用。在Server模式下,它主要還有兩大用途:一個是在JDK1.5及以前的版本中與Parallel Scanvenge收集器搭配使用,另外一個就是作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure的時候使用。

通過指定-UseSerialGC參數(shù),使用Serial + Serial Old的串行收集器組合進(jìn)行內(nèi)存回收。

ParNew收集器

ParNew收集器是Serial收集器新生代的多線程實現(xiàn),注意在進(jìn)行垃圾回收的時候依然會stop the world,只是相比較Serial收集器而言它會運(yùn)行多條進(jìn)程進(jìn)行垃圾回收。

ParNew收集器在單CPU的環(huán)境中絕對不會有比Serial收集器更好的效果,甚至由于存在線程交互的開銷,該收集器在通過超線程技術(shù)實現(xiàn)的兩個CPU的環(huán)境中都不能百分之百的保證能超越Serial收集器。當(dāng)然,隨著可以使用的CPU的數(shù)量增加,它對于GC時系統(tǒng)資源的利用還是很有好處的。它默認(rèn)開啟的收集線程數(shù)與CPU的數(shù)量相同,在CPU非常多(譬如32個,現(xiàn)在CPU動輒4核加超線程,服務(wù)器超過32個邏輯CPU的情況越來越多了)的環(huán)境下,可以使用-XX:ParallelGCThreads參數(shù)來限制垃圾收集的線程數(shù)。

-UseParNewGC: 打開此開關(guān)后,使用ParNew + Serial Old的收集器組合進(jìn)行內(nèi)存回收,這樣新生代使用并行收集器,老年代使用串行收集器。

Parallel Scavenge收集器

Parallel是采用復(fù)制算法的多線程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一個特點是它所關(guān)注的目標(biāo)是吞吐量(Throughput)。所謂吞吐量就是CPU用于運(yùn)行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量=運(yùn)行用戶代碼時間 / (運(yùn)行用戶代碼時間 + 垃圾收集時間)。停頓時間越短就越適合需要與用戶交互的程序,良好的響應(yīng)速度能夠提升用戶的體驗;而高吞吐量則可以最高效率地利用CPU時間,盡快地完成程序的運(yùn)算任務(wù),主要適合在后臺運(yùn)算而不需要太多交互的任務(wù)。

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,采用多線程和”標(biāo)記-整理”算法。這個收集器是在jdk1.6中才開始提供的,在此之前,新生代的Parallel Scavenge收集器一直處于比較尷尬的狀態(tài)。原因是如果新生代Parallel Scavenge收集器,那么老年代除了Serial Old(PS MarkSweep)收集器外別無選擇。由于單線程的老年代Serial Old收集器在服務(wù)端應(yīng)用性能上的”拖累“,即使使用了Parallel Scavenge收集器也未必能在整體應(yīng)用上獲得吞吐量最大化的效果,又因為老年代收集中無法充分利用服務(wù)器多CPU的處理能力,在老年代很大而且硬件比較高級的環(huán)境中,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合”給力“。直到Parallel Old收集器出現(xiàn)后,”吞吐量優(yōu)先“收集器終于有了比較名副其實的應(yīng)用祝賀,在注重吞吐量及CPU資源敏感的場合,都可以優(yōu)先考慮Parallel Scavenge加Parallel Old收集器。

-UseParallelGC: 虛擬機(jī)運(yùn)行在Server模式下的默認(rèn)值,打開此開關(guān)后,使用Parallel Scavenge + Serial Old的收集器組合進(jìn)行內(nèi)存回收。-UseParallelOldGC: 打開此開關(guān)后,使用Parallel Scavenge + Parallel Old的收集器組合進(jìn)行垃圾回收

CMS收集器

CMS(Concurrent Mark Swep)收集器是一個比較重要的回收器,現(xiàn)在應(yīng)用非常廣泛,我們重點來看一下,CMS一種獲取最短回收停頓時間為目標(biāo)的收集器,這使得它很適合用于和用戶交互的業(yè)務(wù)。從名字(Mark Swep)就可以看出,CMS收集器是基于標(biāo)記清除算法實現(xiàn)的。它的收集過程分為四個步驟:

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

并發(fā)標(biāo)記(concurrent mark)

重新標(biāo)記(remark)

并發(fā)清除(concurrent sweep)

注意初始標(biāo)記和重新標(biāo)記還是會stop the world,但是在耗費(fèi)時間更長的并發(fā)標(biāo)記和并發(fā)清除兩個階段都可以和用戶進(jìn)程同時工作。

不過由于CMS收集器是基于標(biāo)記清除算法實現(xiàn)的,會導(dǎo)致有大量的空間碎片產(chǎn)生,在為大對象分配內(nèi)存的時候,往往會出現(xiàn)老年代還有很大的空間剩余,但是無法找到足夠大的連續(xù)空間來分配當(dāng)前對象,不得不提前開啟一次Full GC。為了解決這個問題,CMS收集器默認(rèn)提供了一個-XX:+UseCMSCompactAtFullCollection收集開關(guān)參數(shù)(默認(rèn)就是開啟的),用于在CMS收集器進(jìn)行FullGC完開啟內(nèi)存碎片的合并整理過程,內(nèi)存整理的過程是無法并發(fā)的,這樣內(nèi)存碎片問題倒是沒有了,不過停頓時間不得不變長。虛擬機(jī)設(shè)計者還提供了另外一個參數(shù)-XX:CMSFullGCsBeforeCompaction參數(shù)用于設(shè)置執(zhí)行多少次不壓縮的FULL GC后跟著來一次帶壓縮的(默認(rèn)值為0,表示每次進(jìn)入Full GC時都進(jìn)行碎片整理)。

不幸的是,它作為老年代的收集器,卻無法與jdk1.4中已經(jīng)存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中使用cms來收集老年代的時候,新生代只能選擇ParNew或Serial收集器中的一個。ParNew收集器是使用-XX:+UseConcMarkSweepGC選項啟用CMS收集器之后的默認(rèn)新生代收集器,也可以使用-XX:+UseParNewGC選項來強(qiáng)制指定它。

由于CMS收集器現(xiàn)在比較常用,下面我們再額外了解一下CMS算法的幾個常用參數(shù):

UseCMSInitatingOccupancyOnly:表示只在到達(dá)閾值的時候,才進(jìn)行 CMS 回收。

為了減少第二次暫停的時間,通過-XX:+CMSParallelRemarkEnabled開啟并行remark。如果ramark時間還是過長的話,可以開啟-XX:+CMSScavengeBeforeRemark選項,強(qiáng)制remark之前開啟一次minor gc,減少remark的暫停時間,但是在remark之后也立即開始一次minor gc。

CMS默認(rèn)啟動的回收線程數(shù)目是(ParallelGCThreads + 3)/4,如果你需要明確設(shè)定,可以通過-XX:+ParallelCMSThreads來設(shè)定,其中-XX:+ParallelGCThreads代表的年輕代的并發(fā)收集線程數(shù)目。

CMSClassUnloadingEnabled: 允許對類元數(shù)據(jù)進(jìn)行回收。

CMSInitatingPermOccupancyFraction:當(dāng)永久區(qū)占用率達(dá)到這一百分比后,啟動 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。

CMSIncrementalMode:使用增量模式,比較適合單 CPU。

UseCMSCompactAtFullCollection參數(shù)可以使 CMS 在垃圾收集完成后,進(jìn)行一次內(nèi)存碎片整理。內(nèi)存碎片的整理并不是并發(fā)進(jìn)行的。

UseFullGCsBeforeCompaction:設(shè)定進(jìn)行多少次 CMS 垃圾回收后,進(jìn)行一次內(nèi)存壓縮。

一些建議

對于Native Memory:

使用了NIO或者NIO框架(Mina/Netty)

使用了DirectByteBuffer分配字節(jié)緩沖區(qū)

使用了MappedByteBuffer做內(nèi)存映射

由于Native Memory只能通過FullGC回收,所以除非你非常清楚這時真的有必要,否則不要輕易調(diào)用System.gc()。

另外為了防止某些框架中的System.gc調(diào)用(例如NIO框架、Java RMI),建議在啟動參數(shù)中加上-XX:+DisableExplicitGC來禁用顯式GC。這個參數(shù)有個巨大的坑,如果你禁用了System.gc(),那么上面的3種場景下的內(nèi)存就無法回收,可能造成OOM,如果你使用了CMS GC,那么可以用這個參數(shù)替代:-XX:+ExplicitGCInvokesConcurrent。

此外除了CMS的GC,其實其他針對old gen的回收器都會在對old gen回收的同時回收young gen。

G1收集器

G1收集器是一款面向服務(wù)端應(yīng)用的垃圾收集器。HotSpot團(tuán)隊賦予它的使命是在未來替換掉JDK1.5中發(fā)布的CMS收集器。與其他GC收集器相比,G1具備如下特點:

并行與并發(fā):G1能更充分的利用CPU,多核環(huán)境下的硬件優(yōu)勢來縮短stop the world的停頓時間。

分代收集:和其他收集器一樣,分代的概念在G1中依然存在,不過G1不需要其他的垃圾回收器的配合就可以獨(dú)自管理整個GC堆。

空間整合:G1收集器有利于程序長時間運(yùn)行,分配大對象時不會無法得到連續(xù)的空間而提前觸發(fā)一次GC。

可預(yù)測的非停頓:這是G1相對于CMS的另一大優(yōu)勢,降低停頓時間是G1和CMS共同的關(guān)注點,能讓使用者明確指定在一個長度為M毫秒的時間片段內(nèi),消耗在垃圾收集上的時間不得超過N毫秒。

在使用G1收集器時,Java堆的內(nèi)存布局和其他收集器有很大的差別,它將這個Java堆分為多個大小相等的獨(dú)立區(qū)域,雖然還保留新生代和老年代的概念,但是新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續(xù))的集合。

雖然G1看起來有很多優(yōu)點,實際上CMS還是主流。

與GC相關(guān)的常用參數(shù)

除了上面提及的一些參數(shù),下面補(bǔ)充一些和GC相關(guān)的常用參數(shù):

-Xmx: 設(shè)置堆內(nèi)存的最大值。

-Xms: 設(shè)置堆內(nèi)存的初始值。

-Xmn: 設(shè)置新生代的大小。

-Xss: 設(shè)置棧的大小。

-PretenureSizeThreshold: 直接晉升到老年代的對象大小,設(shè)置這個參數(shù)后,大于這個參數(shù)的對象將直接在老年代分配。

-MaxTenuringThrehold: 晉升到老年代的對象年齡。每個對象在堅持過一次Minor GC之后,年齡就會加1,當(dāng)超過這個參數(shù)值時就進(jìn)入老年代。

-UseAdaptiveSizePolicy: 在這種模式下,新生代的大小、eden 和 survivor 的比例、晉升老年代的對象年齡等參數(shù)會被自動調(diào)整,以達(dá)到在堆大小、吞吐量和停頓時間之間的平衡點。在手工調(diào)優(yōu)比較困難的場合,可以直接使用這種自適應(yīng)的方式,僅指定虛擬機(jī)的最大堆、目標(biāo)的吞吐量 (GCTimeRatio) 和停頓時間 (MaxGCPauseMills),讓虛擬機(jī)自己完成調(diào)優(yōu)工作。

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

-XX:ParallelGCThreads:設(shè)置用于垃圾回收的線程數(shù)。通常情況下可以和 CPU 數(shù)量相等。但在 CPU 數(shù)量比較多的情況下,設(shè)置相對較小的數(shù)值也是合理的。

-XX:MaxGCPauseMills:設(shè)置最大垃圾收集停頓時間。它的值是一個大于 0 的整數(shù)。收集器在工作時,會調(diào)整 Java 堆大小或者其他一些參數(shù),盡可能地把停頓時間控制在 MaxGCPauseMills 以內(nèi)。

-XX:GCTimeRatio:設(shè)置吞吐量大小,它的值是一個 0-100 之間的整數(shù)。假設(shè) GCTimeRatio 的值為 n,那么系統(tǒng)將花費(fèi)不超過 1/(1+n) 的時間用于垃圾收集。

?著作權(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)容