內(nèi)存抖動(dòng)是指內(nèi)存頻繁地分配和回收,而頻繁的gc會(huì)導(dǎo)致卡頓,嚴(yán)重時(shí)和內(nèi)存泄漏一樣會(huì)導(dǎo)致OOM。
內(nèi)存抖動(dòng)為什么會(huì)造成OOM這關(guān)系到Java的垃圾回收。
垃圾回收
在對(duì)對(duì)象進(jìn)行回收前需要對(duì)垃圾進(jìn)行采集,不同的虛擬機(jī)實(shí)現(xiàn)可能使用不同的垃圾收集算法,不同的收集算法的實(shí)現(xiàn)也不盡相同。不同的算法各有各的優(yōu)劣勢(shì)。
常用的收集算法有:
-
標(biāo)記-清除算法 Mark-Sweep
算法分為標(biāo)記和清除兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收被標(biāo)記的對(duì)象。
如上圖所示。標(biāo)記-清除算法不會(huì)進(jìn)行對(duì)象的移動(dòng),直接回收不存活的對(duì)象,因此會(huì)造成內(nèi)存碎片。 -
復(fù)制算法 Copying
“復(fù)制”(Copying)的收集算法,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。
這樣使得每次都是對(duì)其中的一塊進(jìn)行內(nèi)存回收,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可,實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為原來(lái)的一半,持續(xù)復(fù)制長(zhǎng)生存期的對(duì)象則導(dǎo)致效率降低。
復(fù)制收集算法在對(duì)象存活率較高時(shí)就要執(zhí)行較多的復(fù)制操作,效率將會(huì)變低。更關(guān)鍵的是,如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保,以應(yīng)對(duì)被使用的內(nèi)存中所有對(duì)象都100%存活的極端情況。 -
標(biāo)記壓縮算法 Mark-Compact
標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉端邊界以外的內(nèi)存。
標(biāo)記-壓縮算法雖然緩解的內(nèi)存碎片問(wèn)題,但是它也引用了額外的開銷,比如說(shuō)額外的空間來(lái)保存遷移地址,需要遍歷多次堆內(nèi)存等。
4. 分代收集算法
無(wú)論是一般的JVM還是DVM,不會(huì)只使用一種垃圾收集算法。它會(huì)根據(jù)內(nèi)存的劃分實(shí)現(xiàn)不同的收集算法。
當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用分代收集算法。分代的垃圾回收策略,是基于不同的對(duì)象的生命周期是不一樣的。因此,不同生命周期的對(duì)象可以采取不同的收集方式,以便提高回收效率。
在Java程序運(yùn)行的過(guò)程中,會(huì)產(chǎn)生大量的對(duì)象,因每個(gè)對(duì)象所能承擔(dān)的職責(zé)不同所具有的功能不同所以也有著不一樣的生命周期,有的對(duì)象生命周期較長(zhǎng),比如Android中的Application、啟動(dòng)的Service等;有的對(duì)象生命周期較短,比如一些函數(shù)內(nèi)部new出來(lái)的String對(duì)象。
在不進(jìn)行對(duì)象存活時(shí)間區(qū)分的情況下,每次垃圾回收都是對(duì)整個(gè)堆空間進(jìn)行回收,那么消耗的時(shí)間相對(duì)會(huì)很長(zhǎng),而且對(duì)于存活時(shí)間較長(zhǎng)的對(duì)象進(jìn)行的掃描工作等都是徒勞。因此就需要引入分治的思想,所謂分治的思想就是因地制宜,將對(duì)象進(jìn)行代的劃分,把不同生命周期的對(duì)象放在不同的代上使用不同的垃圾回收方式。
現(xiàn)在主流的做法是將Java堆被分為新生代和老年代;
新生代又被進(jìn)一步劃分為Eden和Survivor區(qū), Survivor由From Space和To Space組成
這樣劃分的好處是為了更快的回收內(nèi)存,根據(jù)不同的分代執(zhí)行不同的回收算法;
新生代:
新建的對(duì)象都是用新生代分配內(nèi)存,當(dāng)Eden滿時(shí),會(huì)把存活的對(duì)象轉(zhuǎn)移到兩個(gè)Survivor中的一個(gè),當(dāng)一個(gè)Survivor滿了的時(shí)候會(huì)把不滿足晉升的對(duì)象復(fù)制到另一個(gè)Survivor。
晉升的意思是對(duì)象每經(jīng)歷一次Minor GC (新生代中的gc),年齡+1,年齡達(dá)到設(shè)置的一個(gè)閥值后,被放入老年代。
兩個(gè)Survivor的目的是避免碎片。如果只有一個(gè)Survivor,那Survivor被執(zhí)行一次gc之后,可能對(duì)象是A+B+C。經(jīng)歷一次GC后B被回收。則會(huì)A| |C,造成碎片
老年代:
用于存放新生代中經(jīng)過(guò)N次垃圾回收仍然存活的對(duì)象。
老年代的垃圾回收稱為Major GC。整堆包括新生代與老年代的垃圾回收稱之為Full GC。
持久代:
主要存放所有已加載的類信息,方法信息,常量池等等。并不等同于方法區(qū),只不過(guò)是主流的sun公司的Hotspot JVM用持久帶來(lái)實(shí)現(xiàn)方法區(qū)而已,有些虛擬機(jī)沒(méi)有持久帶而用其他機(jī)制來(lái)實(shí)現(xiàn)方法區(qū)。這個(gè)區(qū)域存放的內(nèi)容與垃圾回收要回收的Java對(duì)象關(guān)系并不大。
一般來(lái)說(shuō),在新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,只有少量存活,所以一般選用復(fù)制算法,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集。
而老年代中因?yàn)閷?duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保,就必須使用“標(biāo)記-清理”或“標(biāo)記-整理”算法來(lái)進(jìn)行回收。
垃圾收集器
垃圾收集算法是內(nèi)存回收的概念,那么垃圾收集器就是內(nèi)存回收的具體實(shí)現(xiàn)。Java虛擬機(jī)規(guī)范對(duì)如何實(shí)現(xiàn)垃圾收集器沒(méi)有任何規(guī)定,所以不同的廠商、不同版本的虛擬機(jī)提供的垃圾收集器可能會(huì)有很大差別。
垃圾收集器主要有:
Serial串行收集器
最基本,歷史最悠久的收集器。曾經(jīng)是jvm新生代的唯一選擇。這個(gè)收集器是一個(gè)單線程的。在進(jìn)行垃圾收集時(shí),必須暫停其他所有的工作線程,直到收集結(jié)束才能繼續(xù)執(zhí)行。
它的缺點(diǎn)也很明顯,會(huì)‘Stop The World’。也就是會(huì)把我們的程序暫停。
但是單線程也意味著它非常簡(jiǎn)單高效,沒(méi)有多余的線程交互,專心收垃圾就可以了。所以在client版本的java中是默認(rèn)的新生代收集器。
ParNew 收集器
Serial收集器的多線程版本,除了使用多線程進(jìn)行垃圾收集之外。其他的行為和Serial一樣。
ParNew收集器是server版本的虛擬機(jī)中首選的新生代收集器。因?yàn)槌薙erial就他可以和CMS配合。
Parallel Scavenge收集器
同樣是新生代的收集器,也同樣是使用復(fù)制算法的,并行的多線程收集器。而它與ParNew等其他收集器差異化的地方在于,它的關(guān)注點(diǎn)在控制吞吐量,也就是cpu用于運(yùn)行用戶代碼事件于cpu總消耗時(shí)間的比值。所以吞吐量=運(yùn)行用戶代碼時(shí)間/(運(yùn)行用戶代碼時(shí)間+垃圾收集時(shí)間)。虛擬機(jī)總共運(yùn)行100分鐘,其中垃圾回收花掉1分鐘,則吞吐量為 99/99+1 = 99%。
而吞吐量越高表示垃圾回收時(shí)間占比越小,cpu利用效率越高。
所以這個(gè)收集器也被稱為”吞吐量收集器”. 高吞吐量為目標(biāo),即減少垃圾收集時(shí)間,讓用戶代碼獲得更長(zhǎng)的運(yùn)行時(shí)間。
Serial Old收集器
老年代版本的串行收集器,使用標(biāo)記整理算法。
Parallel Old收集器
多線程采集,標(biāo)記整理算法。
CMS 收集器
Concurrent Mark Sweep收集器是一種以獲得最短回收停頓事件為目標(biāo)的收集器,也稱為并發(fā)低停頓收集器或低延遲垃圾收集器;。使用的是標(biāo)記清除算法
比前幾個(gè)收集器復(fù)雜很多,可以分為4個(gè)步驟:
(A)、初始標(biāo)記(CMS initial mark)
僅標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快;但需要"Stop The World";
(B)、并發(fā)標(biāo)記(CMS concurrent mark)
進(jìn)行GC Roots 追蹤的過(guò)程;剛才產(chǎn)生的集合中標(biāo)記出存活對(duì)象;
應(yīng)用程序也在運(yùn)行;并不能保證可以標(biāo)記出所有的存活對(duì)象;
(C)、重新標(biāo)記(CMS remark)
為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記變動(dòng)的那一部分對(duì)象的標(biāo)記記錄;
需要"Stop The World",且停頓時(shí)間比初始標(biāo)記稍長(zhǎng),但遠(yuǎn)比并發(fā)標(biāo)記短;
采用多線程并行執(zhí)行來(lái)提升效率;
(D)、并發(fā)清除(CMS concurrent sweep)
回收所有的垃圾對(duì)象。
由于整個(gè)過(guò)程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過(guò)程收集器線程都可以與用戶線程一起工作,所以,從總體上來(lái)說(shuō),CMS收集器的內(nèi)存回收過(guò)程是與用戶線程一起并發(fā)執(zhí)行的。
CMS是一款優(yōu)秀的收集器,它的主要優(yōu)點(diǎn)在名字上已經(jīng)體現(xiàn)出來(lái)了:并發(fā)收集、低停頓。
但是它的缺點(diǎn)在于:
1、造成CPU資源緊張:
從圖中可以看到會(huì)比其他收集器多開線程
2、無(wú)法處理浮動(dòng)垃圾
由于CMS并發(fā)清理階段用戶線程還在運(yùn)行著,伴隨程序運(yùn)行自然就還會(huì)有新的垃圾不斷產(chǎn)生,這一部分垃圾出現(xiàn)在標(biāo)記過(guò)程之后,CMS無(wú)法在當(dāng)次收集中處理掉它們,只好留待下一次GC時(shí)再清理掉。這一部分垃圾就稱為“浮動(dòng)垃圾”。
因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)作使用。
要是CMS運(yùn)行期間預(yù)留的內(nèi)存無(wú)法滿足程序需要,就會(huì)出現(xiàn)一次“Concurrent Mode Failure”失敗,這時(shí)虛擬機(jī)將啟動(dòng)后備預(yù)案:臨時(shí)啟用Serial Old收集器來(lái)重新進(jìn)行老年代的垃圾收集,這樣停頓時(shí)間就很長(zhǎng)了。
3、大量?jī)?nèi)存碎片
來(lái)源“標(biāo)記—清除”算法。
G1收集器
Garbage-First收集器是當(dāng)今收集器技術(shù)發(fā)展最前沿的成果之一,是一款面向服務(wù)端應(yīng)用的垃圾收集器。
圖中可以看到和 CMS差不多,但是G1的采集范圍是整個(gè)堆(新生代老生代)。他把內(nèi)存堆分成多個(gè)大小相等的獨(dú)立區(qū)域,在最后的篩選回收的時(shí)候根據(jù)這些區(qū)域的回收價(jià)值和成本決定是否回收掉內(nèi)存。
由于 Androd 運(yùn)行在移動(dòng)設(shè)備上,內(nèi)存以及電量等諸多方面跟一般的 PC 設(shè)備都有本質(zhì)的區(qū)別 ,一般的 JVM 沒(méi)法滿足移動(dòng)設(shè)備的要求,所以自己根據(jù)這個(gè)規(guī)范開發(fā)了一個(gè)Dalvik 虛擬機(jī)。
Dalvik虛擬機(jī)主要使用標(biāo)記清除算法,也可以選擇使用拷貝算法。這取決于編譯時(shí)期:
http://androidxref.com/4.4_r1/xref/dalvik/vm/Dvm.mk
ART 是在 Android 4.4 中引入的一個(gè)開發(fā)者選項(xiàng),也是 Android 5.0 及更高版本的默認(rèn) Android 運(yùn)行時(shí)。google已不再繼續(xù)維護(hù)和提供 Dalvik 運(yùn)行時(shí),現(xiàn)在 ART 采用了其字節(jié)碼格式。
ART 有多個(gè)不同的 GC 方案,這些方案包括運(yùn)行不同垃圾回收器。默認(rèn)方案是 CMS。
https://source.android.com/devices/tech/dalvik/gc-debug?hl=zh-cn
ART的多種不同GC方案,默認(rèn)是CMS,主要使用粘性CMS和部分CMS,粘性CMS是ART的不移動(dòng)分代垃圾回收器,它僅掃描堆中自上次GC后修改的部分,并自能回收自上次GC后分配的對(duì)象。除CMS方案外,當(dāng)應(yīng)用將進(jìn)程狀態(tài)更改為察覺(jué)不到卡頓的進(jìn)程狀態(tài)(例如:后臺(tái)或緩存)時(shí),ART將執(zhí)行堆壓縮。
這就是內(nèi)存抖動(dòng)為什么會(huì)造成 Android App OOM。
檢測(cè)優(yōu)化內(nèi)存抖動(dòng)
內(nèi)存抖動(dòng)在Android Profile中表現(xiàn)為:
在Profiler的Memory中點(diǎn)擊Recod(AS 3.3),錄制一段內(nèi)存,然后在stop。(在android studio的老版本中是小紅點(diǎn)開啟錄制,結(jié)束也是小紅點(diǎn))
等待一段時(shí)間
- 總數(shù)量
- 內(nèi)存占用大小
- 大概能定位出問(wèn)題代碼在哪,具體的還需要結(jié)合代碼再分析