版權(quán)聲明:本文為斑馬君學(xué)習(xí)總結(jié)文章,轉(zhuǎn)載請(qǐng)注明出處!
一、垃圾回收
上篇博客介紹了Java內(nèi)存運(yùn)行時(shí)區(qū)域的各個(gè)部分,其中程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧三個(gè)區(qū)域隨線(xiàn)程而生,隨線(xiàn)程而滅;棧中的棧幀隨著方法的進(jìn)入和退出而有條不不紊地執(zhí)行著出棧和入棧操作。每一個(gè)棧幀中分配多少內(nèi)存基本上是在類(lèi)結(jié)構(gòu)確定下來(lái)時(shí)就已知的。),因此這幾個(gè)區(qū)域的內(nèi)存分配和回收都具備確定性,在這幾個(gè)區(qū)域內(nèi)就不需要過(guò)多考慮回收的問(wèn)題,因?yàn)榉椒ńY(jié)束或者線(xiàn)程結(jié)束時(shí),內(nèi)存自然就跟隨著回收了。
而Java堆和方法區(qū)則不一樣,一個(gè)接口中的多個(gè)實(shí)現(xiàn)類(lèi)需要的內(nèi)存可能不一樣,一個(gè)方法中的多個(gè)分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運(yùn)行期間時(shí)才能知道會(huì)創(chuàng)建哪些對(duì)象,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的,垃圾收集器所關(guān)注的是這部分內(nèi)存。
GC需要完成的3件事情:
- 如何判定對(duì)象為垃圾對(duì)象
- 如何回收
- 何時(shí)回收
二、如何判定對(duì)象為垃圾對(duì)象
在堆里面存放著Java世界中幾乎所有的對(duì)象實(shí)例,垃圾收集器在對(duì)堆進(jìn)行回收前,第一件事情就是要確定這些對(duì)象之中哪些還“存活”著,哪些已經(jīng)“死去”。
引用計(jì)數(shù)算法
在對(duì)象中添加一個(gè)引用計(jì)數(shù)器,當(dāng)有地方引用這個(gè)對(duì)象的時(shí)候,引用計(jì)數(shù)器的值就+1,當(dāng)引用失效的時(shí)候,計(jì)數(shù)器的值就-1.該算法缺陷是難解決對(duì)象之間相互循環(huán)引用的問(wèn)題。
public class Main {
private Object instance;
public Main() {
byte[] m = new byte[20 * 1024 *1024];
}
public static void main(String[] args) {
Main m1 = new Main();
Main m2 = new Main();
m1.instance = m2;
m2.instance = m1;
m1 = null;
m2 = null;
System.gc();
}
VM arguments參數(shù)設(shè)置:-verbose:gc -XX:+PrintGCDetails
從運(yùn)行結(jié)果中可以清楚看到,GC日志中包含“707K->476K”,意味著虛擬機(jī)并沒(méi)有因?yàn)檫@兩個(gè)對(duì)象互相引用就不回收它們,這也從側(cè)面說(shuō)明虛擬機(jī)并不是通過(guò)引用計(jì)數(shù)算法來(lái)判斷對(duì)象是否存活的。
可達(dá)性分析算法
這個(gè)算法的基本思路就是通過(guò)一些列的稱(chēng)為"GC Roots"的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索所走過(guò)的路徑稱(chēng)為引用鏈,當(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引用的對(duì)象
再談引用
無(wú)論是通過(guò)引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量,還是通過(guò)可達(dá)性分析算法判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá),判定對(duì)象是否存活都與“引用”有關(guān)。希望能描述這樣一類(lèi)對(duì)象:當(dāng)內(nèi)存空間還足夠時(shí),則能保留在內(nèi)存之中;如果內(nèi)存空間在進(jìn)行垃圾收集后還是非常緊張,則可以?huà)仐夁@些對(duì)象。很多系統(tǒng)的緩存功能都符合這樣的應(yīng)用場(chǎng)景。
在JDK 1.2之后,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong
Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(PhantomReference)4種,這4種引用強(qiáng)度依次逐漸減弱。
- 強(qiáng)引用就是指在程序代碼之中普遍存在的,類(lèi)似“Object obj=new Object()”這類(lèi)的引用,只要強(qiáng)引用還存在,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
- 軟引用是用來(lái)描述一些還有用但并非必需的對(duì)象。對(duì)于軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收。如果這次回收還沒(méi)有足夠的內(nèi)存,才會(huì)拋出內(nèi)存溢出異常。在JDK 1.2之后,提供了SoftReference類(lèi)來(lái)實(shí)現(xiàn)軟引用。
- 弱引用也是用來(lái)描述非必需對(duì)象的,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生之前。當(dāng)垃圾收集器工作時(shí),無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。在JDK 1.2之后,提供了WeakReference類(lèi)來(lái)實(shí)現(xiàn)弱引用。
三、如何回收
回收策略:標(biāo)記-清除算法
算法分為”標(biāo)記“和”清除“兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。
該算法主要有兩個(gè)不足的問(wèn)題:
效率問(wèn)題:標(biāo)記和清除兩個(gè)過(guò)程的效率都不高;
空間問(wèn)題:標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行過(guò)程中需要分配較大對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。
回收策略:復(fù)制算法
該算法將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。



回收策略:標(biāo)記-整理算法和分代收集算法
復(fù)制收集算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作,效率將會(huì)變低。更關(guān)鍵的是,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
“標(biāo)記-清除”算法:不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。

四、垃圾收集器
新生代收集器-Serial收集器
是最基本、發(fā)展歷史最悠久的收集器。曾經(jīng)(在JDK 1.3.1之前)是虛擬機(jī)新生代收集的唯一選擇。這個(gè)收集器是一個(gè)單線(xiàn)程的收集器,但它的“單線(xiàn)程”的意義并不僅僅說(shuō)明它只會(huì)使用一個(gè)CPU或一條收集線(xiàn)程去完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集時(shí),必須暫停其他所有的工作線(xiàn)程,直到它收集結(jié)束。

Serial收集器可用的所有控制參數(shù)(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)
新生代收集器-ParNew收集器
ParNew收集器其實(shí)就是Serial收集器的多線(xiàn)程版本,除了使用多條線(xiàn)程進(jìn)行垃圾收集外,其余與Serial收集器完全一樣。ParNew收集器是許多運(yùn)行在Server模式下的虛擬機(jī)中首選的新生代收集器。

使用-XX:ParallelGCThreads參數(shù)來(lái)限制垃圾收集的線(xiàn)程數(shù)。使用-XX:+UseConcMarkSweepGC選項(xiàng)后的默認(rèn)新生代收集器,也可以使用-XX:+UseParNewGC選項(xiàng)來(lái)強(qiáng)制指定它。
新生代收集器-Parallel Scavenge收集器
Parallel Scavenge收集器是一個(gè)新生代收集器,使用復(fù)制算法的并行收集器,Parallel Scavenge 收集器使用兩個(gè)參數(shù)控制吞吐量。
直觀上,只要最大的垃圾收集停頓時(shí)間越小,吞吐量是越高的,但是GC停頓時(shí)間的縮短是以犧牲吞吐量和新生代空間作為代價(jià)的。比如原來(lái)10秒收集一次,每次停頓100毫秒。但是線(xiàn)程編程每5秒收集一次,每次停頓70毫秒,停頓時(shí)間下降的同時(shí),吞吐量也下降了。
停頓時(shí)間越短就越適合需要與用戶(hù)交互的程序,良好的響應(yīng)速度能提升用戶(hù)體驗(yàn),而高吞吐量則可以高效地利用CPU時(shí)間,盡快完成程序的運(yùn)算任務(wù),主要適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)。
Parallel Scavenge收集器提供了兩個(gè)參數(shù)用于精確控制吞吐量,分別是控制最大垃圾收集停頓時(shí)間的-XX:MaxGCPauseMillis參數(shù)以及直接設(shè)置吞吐量大小的-XX:GCTimeRatio參數(shù)。收集器有一個(gè)參數(shù)- XX:+UseAdaptiveSizePolicy當(dāng)這個(gè)參數(shù)打開(kāi)之后,就不需要手動(dòng)指定新生代的大小,Eden和Survivor區(qū)的比例,晉升老年代對(duì)象等細(xì)節(jié)參數(shù)了,虛擬機(jī)會(huì)根據(jù)當(dāng)前系統(tǒng)的運(yùn)行情況收集性能監(jiān)控信息,動(dòng)態(tài)調(diào)整這些參數(shù)以提供最合適的停頓時(shí)間或者最大吞吐量,這種調(diào)節(jié)方式成為GC自適應(yīng)的調(diào)節(jié)策略。
老年代收集器 - CMS收集器
由于垃圾回收時(shí),都需要暫停用戶(hù)線(xiàn)程,CMS(Concurrent Mark Sweep)收集器是一種以 獲取最短停頓時(shí)間 為目標(biāo)的收集器,重視服務(wù)的響應(yīng)速度,希望系統(tǒng)停頓時(shí)間最短,能給用戶(hù)帶來(lái)良好的體驗(yàn)。
CMS收集器是基于"標(biāo)記-清除"算法實(shí)現(xiàn)的,它的運(yùn)作過(guò)程比較復(fù)雜,整個(gè)過(guò)程分為四個(gè)步驟:
- 初始標(biāo)記 初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快,需要Stop The World(暫停所有的用戶(hù)線(xiàn)程)
- 并發(fā)標(biāo)記 并發(fā)標(biāo)記階段就是進(jìn)行GC Roots Tracing的過(guò)程 (用戶(hù)不暫停)—用戶(hù)不暫停就還可能產(chǎn)生一些對(duì)象與GC Roots不可達(dá)
- 重新標(biāo)記重新標(biāo)記階段是為了修正 并發(fā)標(biāo)記期間 因用戶(hù)程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng) 的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間會(huì)比初始階段稍長(zhǎng)一些,但是遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短,仍然需要"Stop The World"
- 并發(fā)清除并發(fā)清除階段會(huì)清除對(duì)象(用戶(hù)不暫停)
整個(gè)過(guò)程中耗時(shí)最長(zhǎng)的并發(fā)表及和并發(fā)清除過(guò)程收集線(xiàn)程可以與用戶(hù)線(xiàn)程一起工作,所以整體上來(lái)說(shuō),CMS收集器的內(nèi)存回收過(guò)程與用戶(hù)線(xiàn)程一起并發(fā)執(zhí)行。
優(yōu)點(diǎn):CMS是一款優(yōu)秀的收集器,主要優(yōu)點(diǎn):并發(fā)、低停頓。
缺點(diǎn):
- CMS收集器對(duì)CPU的資源非常敏感,在并發(fā)階段,它雖然不會(huì)導(dǎo)致用戶(hù)線(xiàn)程停頓,但是會(huì)因?yàn)?strong>占了一部分CPU資源,而導(dǎo)致應(yīng)用程序變慢,總吞吐量會(huì)降低。
- CMS無(wú)法處理浮動(dòng)垃圾,由于CMS 并發(fā)清理階段用戶(hù)線(xiàn)程還在運(yùn)行著,用戶(hù)線(xiàn)程在運(yùn)行自然就還會(huì)有新的垃圾產(chǎn)生,CMS無(wú)法在當(dāng)次收集中處理掉它們,只好留到下一次GC再清理掉,這一部分垃圾叫做"浮動(dòng)垃圾"。
- CMS收集器會(huì)產(chǎn)生大量的空間碎片,CMS是一款基于"標(biāo)記-清除" 算法實(shí)現(xiàn)的收集器,這意味著收集結(jié)束時(shí)會(huì)有大量空間碎片產(chǎn)生??臻g碎片過(guò)多時(shí),就會(huì)給大對(duì)象的分配帶來(lái)很多麻煩,往往會(huì)出現(xiàn)還有很大的空間剩余,但是無(wú)法找到足夠大連續(xù)的空間來(lái)分配當(dāng)前對(duì)象,不得不提前觸發(fā)一次Full GC。
全區(qū)域的垃圾回收器 - G1收集器
G1垃圾回收器是用在heap memory很大的情況下,把heap劃分為很多很多的region塊,然后并行的對(duì)其進(jìn)行垃圾回收。
G1垃圾回收器在清除實(shí)例所占用的內(nèi)存后,還會(huì)做內(nèi)存壓縮。
G1垃圾回收器回收region的時(shí)候基本不會(huì)Stop The World,從整體來(lái)看是基于標(biāo)記-整理算法,從局部(兩個(gè)region之間)來(lái)看基于復(fù)制算法。

年輕代垃圾收集
在G1垃圾收集器中,年輕代的垃圾回收過(guò)程使用復(fù)制算法,把Eden區(qū)和Survivor區(qū)的對(duì)象復(fù)制到新的Survivor區(qū)域。

對(duì)于老年代的垃圾收集,G1(Garbage First)也分為四個(gè)階段,基本與CMS垃圾收集器一樣,但是略有不同。
- 初始標(biāo)記(Initial Mark) 同CMS垃圾收集器初始標(biāo)記階段一樣,G1也需要暫停應(yīng)用程序的執(zhí)行,它會(huì)標(biāo)記從跟對(duì)象出發(fā),在根對(duì)象的第一層孩子結(jié)點(diǎn)中標(biāo)記所有可達(dá)對(duì)象。但是G1的垃圾收集器的初始標(biāo)記結(jié)點(diǎn)是跟Minor gc一起發(fā)生的。也就是說(shuō),在G1中,不用像CMS那樣,單獨(dú)暫停應(yīng)用程序的執(zhí)行來(lái)運(yùn)行初始標(biāo)記階段,而是在G1出發(fā)Minor gc的時(shí)候一并將老年代上的初始標(biāo)記給做了。
- 并發(fā)標(biāo)記(Concurrent Mark) 同CMS垃圾收集器并發(fā)標(biāo)記階段一樣,但G1還多做了一件事件,就是如果在并發(fā)標(biāo)記階段,發(fā)現(xiàn)哪些Tenured region中對(duì)象的存活率很小或者基本沒(méi)有對(duì)象存活,那么G1就會(huì)在這個(gè)階段將其回收掉,而不用等到后面的清除階段,這也是Garbage First名字的由來(lái),同時(shí)在該階段,G1會(huì)計(jì)算每個(gè)region的存活率,方便后面的清除階段使用。
- 最終標(biāo)記(CMS中的remark階段) 同CMS垃圾收集器重新標(biāo)記階段一樣,但是采用的算法不一樣,G1采用了一種叫做STAB(snapshot-at-the-begining)的算法能夠在Remark階段更快的標(biāo)記可達(dá)對(duì)象。
-
篩選回收(clean up/Copy) 在G1中,沒(méi)有CMS對(duì)于的Sweep階段。相反,它有一個(gè)Clean up/Copy階段,在這個(gè)階段中,G1會(huì)挑選出那么對(duì)象存活率低的region進(jìn)行回收,這個(gè)階段也是和minor gc一同完成的。
G1是一款面向服務(wù)端應(yīng)用的垃圾收集器,Hotspot開(kāi)發(fā)團(tuán)隊(duì)賦予它的使命是未來(lái)可以替換掉JDK1.5中發(fā)布的CMS收集器
你想追求低停頓、想讓用戶(hù)有更好的體驗(yàn)用G1
如果你的應(yīng)用追求吞吐量,G1并不能帶來(lái)很明顯的好處。
吞吐量
吞吐量就是CPU 運(yùn)行用戶(hù)代碼的時(shí)間 與 CPU總消耗時(shí)間 的比值。
吞吐量 = 運(yùn)行用戶(hù)代碼的時(shí)間 / (運(yùn)行用戶(hù)代碼的時(shí)間+垃圾收集的時(shí)間)
假設(shè)虛擬機(jī)總共運(yùn)行了100分鐘,其中垃圾收集花了一分鐘 吞吐量就是99%,虛擬機(jī)總共運(yùn)行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。停頓時(shí)間越短就越適合需要與用戶(hù)交互的程序,良好的響應(yīng)速度能提升用戶(hù)體驗(yàn),而高吞吐量則可以高效率地利用CPU時(shí)間,盡快完成程序的運(yùn)算任務(wù)。
五、理解GC日志
閱讀GC日志是處理Java虛擬機(jī)內(nèi)存問(wèn)題的基礎(chǔ)技能,它只是一些人為確定的規(guī)則,沒(méi)有太多技術(shù)含量。
最前面的數(shù)字“33.125:”和“100.667:”代表了GC發(fā)生的時(shí)間,這個(gè)數(shù)字的含義是從Java
虛擬機(jī)啟動(dòng)以來(lái)經(jīng)過(guò)的秒數(shù)。
GC日志開(kāi)頭的“[GC”和“[Full GC”說(shuō)明了這次垃圾收集的停頓類(lèi)型,而不是用來(lái)區(qū)分新生代GC還是老年代GC的。如果有“Full”,說(shuō)明這次GC是發(fā)生了Stop-The-World的,例如下面這段新生代收集器ParNew的日志也會(huì)出現(xiàn)“[Full GC”(這一般是因?yàn)槌霈F(xiàn)了分配擔(dān)保失敗之類(lèi)的問(wèn)題,所以才導(dǎo)致STW)。如果是調(diào)用System.gc()方法所觸發(fā)的收集,那么在這里將顯示“[Full GC(System)”。
接下來(lái)的“[DefNew”、“[Tenured”、“[Perm”表示GC發(fā)生的區(qū)域,這里顯示的區(qū)域名稱(chēng)與使用的GC收集器是密切相關(guān)的,例如上面樣例所使用的Serial收集器中的新生代名為“DefaultNew Generation”,所以顯示的是“[DefNew”。如果是ParNew收集器,新生代名稱(chēng)就會(huì)變?yōu)椤癧ParNew”,意為“Parallel New Generation”。如果采用Parallel Scavenge收集器,那它配套的新生代稱(chēng)為“PSYoungGen”,老年代和永久代同理,名稱(chēng)也是由收集器決定的。
后面方括號(hào)內(nèi)部的“3324K->152K(3712K)”含義是“GC前該內(nèi)存區(qū)域已使用容量->GC后該內(nèi)存區(qū)域已使用容量(該內(nèi)存區(qū)域總?cè)萘浚?。而在方括?hào)之外的“3324K->152K(11904K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆總?cè)萘浚薄?/p>
再往后,“0.0025925 secs”表示該內(nèi)存區(qū)域GC所占用的時(shí)間,單位是秒。有的收集器會(huì)給出更具體的時(shí)間數(shù)據(jù),如“[Times:user=0.01 sys=0.00,real=0.02 secs]”,這里面的user、sys和real與Linux的time命令所輸出的時(shí)間含義一致,分別代表用戶(hù)態(tài)消耗的CPU時(shí)間、內(nèi)核態(tài)消耗的CPU事件和操作從開(kāi)始到結(jié)束所經(jīng)過(guò)的墻鐘時(shí)間(Wall Clock Time)。CPU時(shí)間與墻鐘時(shí)間的區(qū)別是,墻鐘時(shí)間包括各種非運(yùn)算的等待耗時(shí),例如等待磁盤(pán)I/O、等待線(xiàn)程阻塞,而CPU時(shí)間不包括這些耗時(shí),但當(dāng)系統(tǒng)有多CPU或者多核的話(huà),多線(xiàn)程操作會(huì)疊加這些
CPU時(shí)間,所以讀者看到user或sys時(shí)間超過(guò)real時(shí)間是完全正常的。
[GC (Allocation Failure) [DefNew: 707K->476K(4928K), 0.0013631 secs]
[Tenured: 0K->476K(10944K), 0.0024463 secs] 707K->476K(15872K),
[Metaspace: 1640K->1640K(4480K)], 0.0238666 secs] [Times: user=0.00
sys=0.00, real=0.02 secs]
[GC (Allocation Failure) [DefNew: 0K->0K(4992K), 0.0003036 secs]
[Tenured: 20956K->475K(31428K), 0.0017487 secs] 20956K-
>475K(36420K), [Metaspace: 1640K->1640K(4480K)], 0.0021762 secs]
[Times: user=0.02 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [Tenured: 20955K->475K(31428K), 0.0019586 secs] 21208K->475K(45636K), [Metaspace: 1640K->1640K(4480K)], 0.0185442 secs] [Times: user=0.00 sys=0.00, real=0.02 secs]
Heap
def new generation total 13248K, used 235K [0x03e00000, 0x04c60000, 0x09350000)
eden space 11776K, 2% used [0x03e00000, 0x03e3af90, 0x04980000)
from space 1472K, 0% used [0x04980000, 0x04980000, 0x04af0000)
to space 1472K, 0% used [0x04af0000, 0x04af0000, 0x04c60000)
tenured generation total 29380K, used 475K [0x09350000, 0x0b001000, 0x13e00000)
the space 29380K, 1% used [0x09350000, 0x093c6e10, 0x093c7000, 0x0b001000)
Metaspace used 1644K, capacity 2242K, committed 2368K, reserved 4480K

