2020-07-29

一、垃圾回收的意義

在探討Java垃圾回收機(jī)制之前,我們首先應(yīng)該記住一個單詞:Stop-the-World。Stop-the-world意味著 JVM由于要執(zhí)行GC而停止了應(yīng)用程序的執(zhí)行,并且這種情形會在任何一種GC算法中發(fā)生。當(dāng)Stop-the-world發(fā)生時,除了GC所需的線程以外,所有線程都處于等待狀態(tài)直到GC任務(wù)完成。事實上,GC優(yōu)化很多時候就是指減少Stop-the-world發(fā)生的時間,從而使系統(tǒng)具有 高吞吐 、低停頓 的特點。

內(nèi)存泄露:一個對象的存活周期超出了程序需要它的時間長度,例如對內(nèi)存管理較自由的C C++都容易導(dǎo)致內(nèi)存泄漏現(xiàn)象的發(fā)生

二、判斷對象是否可以被回收

1.引用計數(shù)法

引用計數(shù)算法是通過判斷對象的引用數(shù)量來決定對象是否可以被回收。引用計數(shù)收集器可以很快的執(zhí)行,并且交織在程序運行中,對程序需要不被長時間打斷的實時環(huán)境比較有利,但其很難解決對象之間相互循環(huán)引用的問題。如下面的程序和示意圖所示,對象objA和objB之間的引用計數(shù)永遠(yuǎn)不可能為 0,那么這兩個對象就永遠(yuǎn)不能被回收。

引用計數(shù).png

可達(dá)性分析算法

可達(dá)性分析算法是通過判斷對象的引用鏈?zhǔn)欠窨蛇_(dá)來決定對象是否可以被回收。程序把所有的引用關(guān)系看作一張圖,通過一系列的名為 “GC Roots” 的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain)。當(dāng)一個對象到 GC Roots 沒有任何引用鏈相連(用圖論的話來說就是從 GC Roots 到這個對象不可達(dá))時,則證明此對象是不可用的,如下圖所示。在Java中,可作為 GC Root 的對象包括以下幾種:

虛擬機(jī)棧(棧幀中的局部變量表)中引用的對象;

本地方法棧中Native方法引用的對象;

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

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

可達(dá)性分析算法示意圖.jpg

三、垃圾收集算法

標(biāo)記清除算法

標(biāo)記-清除算法分為標(biāo)記和清除兩個階段。該算法首先從根集合進(jìn)行掃描,對存活的對象對象標(biāo)記,標(biāo)記完畢后,再掃描整個空間中未被標(biāo)記的對象并進(jìn)行回收

標(biāo)記清除算法.jpg

該算法的不足:

效率問題: 標(biāo)記和清除兩個過程的效率都不高;

空間問題: 標(biāo)記-清除算法不需要進(jìn)行對象的移動,并且僅對不存活的對象進(jìn)行處理,因此標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。

復(fù)制算法

復(fù)制算法將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。這種算法適用于對象存活率低的場景,比如新生代。這樣使得每次都是對整個半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動堆頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。

新生代中的對象每次回收都基本上只有10%左右的對象存活,所以需要復(fù)制的對象很少,效率還不錯。實踐中會將新生代內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間 (如下圖所示),每次使用Eden和其中一塊Survivor。當(dāng)回收時,將Eden和Survivor中還存活著的對象一次地復(fù)制到另外一塊Survivor空間上,最后清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是 8:1,也就是每次新生代中可用內(nèi)存空間為整個新生代容量的90% ( 80%+10% ),只有10% 的內(nèi)存會被“浪費”。

java堆.JPG

標(biāo)記整理算法

復(fù)制收集算法在對象存活率較高時就要進(jìn)行較多的復(fù)制操作,效率將會變低。更關(guān)鍵的是,如果不想浪費50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對被使用的內(nèi)存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。標(biāo)記整理算法的標(biāo)記過程類似標(biāo)記清除算法,但后續(xù)步驟不是直接對可回收對象進(jìn)行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存,類似于磁盤整理的過程,該垃圾回收算法適用于對象存活率高的場景(老年代),其作用原理如下圖所示。

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

分代收集算法

對于一個大型的系統(tǒng),當(dāng)創(chuàng)建的對象和方法變量比較多時,堆內(nèi)存中的對象也會比較多,如果逐一分析對象是否該回收,那么勢必造成效率低下。分代收集算法是基于這樣一個事實:不同的對象的生命周期(存活情況)是不一樣的,而不同生命周期的對象位于堆中不同的區(qū)域,因此對堆內(nèi)存不同區(qū)域采用不同的策略進(jìn)行回收可以提高 JVM 的執(zhí)行效率。當(dāng)代商用虛擬機(jī)使用的都是分代收集算法:新生代對象存活率低,就采用復(fù)制算法;老年代存活率高,就用標(biāo)記清除算法或者標(biāo)記整理算法。Java堆內(nèi)存一般可以分為新生代、老年代兩個模塊,如下圖所示:

分代收集算法總.jpg

新生代

新生代的目標(biāo)就是盡可能快速的收集掉那些生命周期短的對象,一般情況下,所有新生成的對象首先都是放在新生代的。新生代內(nèi)存按照 8:1:1 的比例分為一個eden區(qū)和兩個survivor(survivor0,survivor1)區(qū),大部分對象在Eden區(qū)中生成。在進(jìn)行垃圾回收時,先將eden區(qū)存活對象復(fù)制到survivor0區(qū),然后清空eden區(qū),當(dāng)這個survivor0區(qū)也滿了時,則將eden區(qū)和survivor0區(qū)存活對象復(fù)制到survivor1區(qū),然后清空eden和這個survivor0區(qū),此時survivor0區(qū)是空的,然后交換survivor0區(qū)和survivor1區(qū)的角色(即下次垃圾回收時會掃描Eden區(qū)和survivor1區(qū)),即保持survivor0區(qū)為空,如此往復(fù)。特別地,當(dāng)survivor1區(qū)也不足以存放eden區(qū)和survivor0區(qū)的存活對象時,就將存活對象直接存放到老年代。如果老年代也滿了,就會觸發(fā)一次FullGC,也就是新生代、老年代都進(jìn)行回收。注意,新生代發(fā)生的GC也叫做MinorGC,MinorGC發(fā)生頻率比較高,不一定等 Eden區(qū)滿了才觸發(fā)。

老年代

老年代存放的都是一些生命周期較長的對象,就像上面所敘述的那樣,在新生代中經(jīng)歷了N次垃圾回收后仍然存活的對象就會被放到老年代中。此外,老年代的內(nèi)存也比新生代大很多(大概比例是1:2),當(dāng)老年代滿時會觸發(fā)Major GC(Full GC),老年代對象存活時間比較長,因此FullGC發(fā)生的頻率比較低。

總結(jié)

收集算法總結(jié).png

垃圾回收的兩種類型:

Minor GC:對新生代進(jìn)行回收,不會影響到年老代。因為新生代的 Java 對象大多死亡頻繁,所以 Minor GC 非常頻繁,一般在這里使用速度快、效率高的算法,使垃圾回收能盡快完成。

Full GC:也叫 Major GC,對整個堆進(jìn)行回收,包括新生代和老年代。由于Full GC需要對整個堆進(jìn)行回收,所以比Minor GC要慢,因此應(yīng)該盡可能減少Full GC的次數(shù),導(dǎo)致Full GC的原因包括:老年代被寫滿、永久代(Perm)被寫滿和System.gc()被顯式調(diào)用等。

四、內(nèi)存分配與回收策略

對象優(yōu)先在Eden分配,當(dāng)Eden區(qū)沒有足夠空間進(jìn)行分配時,虛擬機(jī)將發(fā)起一次MinorGC?,F(xiàn)在的商業(yè)虛擬機(jī)一般都采用復(fù)制算法來回收新生代,將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。 當(dāng)進(jìn)行垃圾回收時,將Eden和Survivor中還存活的對象一次性地復(fù)制到另外一塊Survivor空間上,最后處理掉Eden和剛才的Survivor空間。(HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1)當(dāng)Survivor空間不夠用時,需要依賴?yán)夏甏M(jìn)行分配擔(dān)保。

大對象直接進(jìn)入老年代。所謂的大對象是指,需要大量連續(xù)內(nèi)存空間的Java對象,最典型的大對象就是那種很長的字符串以及數(shù)組。

長期存活的對象將進(jìn)入老年代。當(dāng)對象在新生代中經(jīng)歷過一定次數(shù)(默認(rèn)為15)的Minor GC后,就會被晉升到老年代中。

動態(tài)對象年齡判定。為了更好地適應(yīng)不同程序的內(nèi)存狀況,虛擬機(jī)并不是永遠(yuǎn)地要求對象年齡必須達(dá)到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象就可以直接進(jìn)入老年代,無須等到MaxTenuringThreshold中要求的年齡。

五、G1收集器和CMS收集器

垃圾回收器從線程運行情況分類有三種:

串行回收,Serial回收器,單線程回收,全程stw

并行回收,名稱以Parallel開頭的回收器,多線程回收,全程stw

并發(fā)回收,cms與g1,多線程分階段回收,只有某階段會stw

CMS垃圾回收的特點

cms只回收老年代和永久代,不會收集年輕代

cms是一種預(yù)處理垃圾回收器,它必須要在老年代內(nèi)存用盡前進(jìn)行回收,默認(rèn)是老年代或永久代達(dá)到92%。

CMS垃圾回收的過程

CMS處理的四大步:

初始標(biāo)記(init-mark),會導(dǎo)致stw

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

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

并發(fā)清除(concurrent-sweep),與用戶線程同時運行,這里用的是標(biāo)記清除算法。

注:并發(fā)性帶來的是有新的對象或垃圾產(chǎn)生,所以我們在并發(fā)標(biāo)記之后重新進(jìn)行了標(biāo)記;當(dāng)我們并發(fā)清理之后,由于工作線程和垃圾回收線程是共同運行的,這里就有可能出現(xiàn)在垃圾回收后,還有未被回收的垃圾,我們稱之為“浮動垃圾”。

G1垃圾回收算法

https://www.cnblogs.com/rgever/p/9534857.html

該算法在JDK7u4版本被正式推出,在滿足高吞吐量的同時,盡可能的滿足垃圾回收時的暫停時間。

在G1算法中,采用了另外一套完全不同的方式組織堆內(nèi)存,堆內(nèi)存被劃分為多個大小相等的內(nèi)存塊(Region).

每個Region被標(biāo)記了E、S、O和H,說明每個Region在運行時都充當(dāng)了一種角色,其中H是以往算法中沒有的,它代表Humongous,這表示這些Region存儲的是巨型對象(humongous object,H-obj),當(dāng)新建對象大小超過Region大小一半時,直接在新的一個或多個連續(xù)Region中分配,并標(biāo)記為H。

GC模式

G1中提供了三種垃圾回收模式,young gc、mixed gc和full gc,在不同的條件下被觸發(fā)。

young gc

和普通的minor gc差不多,當(dāng)young region中的內(nèi)存消耗殆盡時,執(zhí)行一次young gc

mixed gc

當(dāng)越來越多的對象晉升到老年代old region時,為了避免堆內(nèi)存被耗盡,虛擬機(jī)會觸發(fā)一個混合的垃圾收集器,即mixed gc,該算法并不是一個old gc,除了回收整個young region,還會回收一部分的old region,這里需要注意:是一部分老年代,而不是全部老年代,可以選擇哪些old region進(jìn)行收集,從而可以對垃圾回收的耗時時間進(jìn)行控制。

這里mixed gc的執(zhí)行過程與cms較為相似,分為四步

full gc

如果對象內(nèi)存分配速度過快,mixed gc來不及回收,導(dǎo)致老年代被填滿,就會觸發(fā)一次full gc,G1的full gc算法就是單線程執(zhí)行的serial old gc,會導(dǎo)致常長時間的暫停,需要進(jìn)行不斷的調(diào)優(yōu),盡可能的避免full gc.

內(nèi)存泄露分析

常用命令:jstat -gcutil pid

查看gc統(tǒng)計信息

2500 代表間隔多久顯示一次

70 代表顯示幾次

S0 — Heap上的 Survivor space 0 區(qū)已使用空間的百分比

S1 — Heap上的 Survivor space 1 區(qū)已使用空間的百分比

E — Heap上的 Eden space 區(qū)已使用空間的百分比

O — Heap上的 Old space 區(qū)已使用空間的百分比

P — Perm space 區(qū)已使用空間的百分比

YGC — 從應(yīng)用程序啟動到采樣時發(fā)生 Young GC 的次數(shù)

YGCT– 從應(yīng)用程序啟動到采樣時 Young GC 所用的時間(單位秒)

FGC — 從應(yīng)用程序啟動到采樣時發(fā)生 Full GC 的次數(shù)

FGCT– 從應(yīng)用程序啟動到采樣時 Full GC 所用的時間(單位秒)

GCT — 從應(yīng)用程序啟動到采樣時用于垃圾回收的總時間(單位秒)

如果有大量的FGC就要查詢是否有內(nèi)存泄漏的問題了,圖中的FGC數(shù)量就比較大,并且執(zhí)行時間較長,這樣就會導(dǎo)致系統(tǒng)的響應(yīng)時間較長,如果對jvm的內(nèi)存設(shè)置較大,那么執(zhí)行一次FGC的時間可能會更長

定位內(nèi)存泄露

使用java自帶的jmap方法,獲取內(nèi)存某一時刻的快照,導(dǎo)出為dump文件后,可以進(jìn)行分析定位。

使用MemoryAnalyzer工具進(jìn)行分析

其中深藍(lán)色的部分為內(nèi)存泄露部分,其占用了java堆60%的空間。

從上圖我們可以發(fā)現(xiàn)紅線圈著的方法占用了堆內(nèi)存的67.75%,如果能把這個測試結(jié)果交給開發(fā),開發(fā)是不是應(yīng)該很好定位呢。所以作為一名高級測試工程師,我們需要學(xué)習(xí)的東西太多。

轉(zhuǎn)載自-->作者:ZMRWEGo

鏈接:http://m.itdecent.cn/p/73663ea9589b

來源:簡書

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

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