內存抖動、OOM
內存抖動
內存抖動是指內存頻繁地分配和回收,而頻繁的gc會導致卡頓,嚴重時和內存泄漏一樣會導致OOM。
內存抖動爲什麼會造成OOM這關係到Java的垃圾回收。
垃圾回收
在對對象進行回收前需要對垃圾進行採集,不同的虛擬機實現可能使用不同的垃圾收集算法,不同的收集算法的實現也不盡相同。不同的算法各有各的優劣勢。
常用的收集算法有:
標記-清除算法 Mark-Sweep
和他的名字一樣,算法分爲標記和清除兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收被標記的對象。
如上圖所示。標記-清除算法不會進行對象的移動,直接回收不存活的對象,因此會造成內存碎片。
根據上圖,比如我們回收後,如果我需要創建一個佔了10個格子的內存大小的對象,這種情況,oom了。因爲雖然我們現在有這麼大的內存可以使用,但是沒有連續的這麼大的內存。
複製算法 Copying
“複製”(Copying)的收集算法,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉。
這樣使得每次都是對其中的一塊進行內存回收,內存分配時也就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半,持續複製長生存期的對象則導致效率降低。
複製收集算法在對象存活率較高時就要執行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端情況。
標記壓縮算法 Mark-Compact
標記過程仍然與“標記-清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。
標記-壓縮算法雖然緩解的內存碎片問題,但是它也引用了額外的開銷,比如說額外的空間來保存遷移地址,需要遍歷多次堆內存等。
分代收集算法
無論是一般的JVM還是DVM,不會只使用一種垃圾收集算法。它會根據內存的劃分實現不同的收集算法。
當前商業虛擬機的垃圾收集都採用分代收集算法。分代的垃圾回收策略,是基於不同的對象的生命週期是不一樣的。因此,不同生命週期的對象可以採取不同的收集方式,以便提高回收效率。
在Java程序運行的過程中,會產生大量的對象,因每個對象所能承擔的職責不同所具有的功能不同所以也有着不一樣的生命週期,有的對象生命週期較長,比如Android中的Application、啓動的Service等;有的對象生命週期較短,比如一些函數內部new出來的String對象。
在不進行對象存活時間區分的情況下,每次垃圾回收都是對整個堆空間進行回收,那麼消耗的時間相對會很長,而且對於存活時間較長的對象進行的掃描工作等都是徒勞。因此就需要引入分治的思想,所謂分治的思想就是因地制宜,將對象進行代的劃分,把不同生命週期的對象放在不同的代上使用不同的垃圾回收方式。
現在主流的做法是將Java堆被分爲新生代和老年代;
新生代又被進一步劃分爲Eden和Survivor區, Survivor由From Space和To Space組成
這樣劃分的好處是爲了更快的回收內存,根據不同的分代執行不同的回收算法;
新生代:
新建的對象都是用新生代分配內存,當Eden滿時,會把存活的對象轉移到兩個Survivor中的一個,當一個Survivor滿了的時候會把不滿足晉升的對象複製到另一個Survivor。
晉升的意思是對象每經歷一次Minor GC (新生代中的gc),年齡+1,年齡達到設置的一個閥值後,被放入老年代。
兩個Survivor的目的是避免碎片。如果只有一個Survivor,那Survivor被執行一次gc之後,可能對象是A+B+C。經歷一次GC後B被回收。則會A| |C,造成碎片
老年代:
用於存放新生代中經過N次垃圾回收仍然存活的對象。
老年代的垃圾回收稱爲Major GC。整堆包括新生代與老年代的垃圾回收稱之爲Full GC。
永久代:
主要存放所有已加載的類信息,方法信息,常量池等等。並不等同於方法區,只不過是主流的日公司的Hotspot JVM用永久帶來實現方法區而已,有些虛擬機沒有永久帶而用其他機制來實現方法區。這個區域存放的內容與垃圾回收要回收的Java對象關係並不大。
一般來說,在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,所以一般選用複製算法,只需要付出少量存活對象的複製成本就可以完成收集。
而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或“標記-整理”算法來進行回收。
垃圾收集器
垃圾收集算法是內存回收的概念,那麼垃圾收集器就是內存回收的具體實現。Java虛擬機規範對如何實現垃圾收集器沒有任何規定,所以不同的廠商、不同版本的虛擬機提供的垃圾收集器可能會有很大差別。
垃圾收集器主要有:
Serial串行收集器
最基本,歷史最悠久的收集器。曾經是jvm新生代的唯一選擇。看名字就知道這個收集器是一個單線程的。在進行垃圾收集時,必須暫停其他所有的工作線程,直到收集結束才能繼續執行。
它的缺點也很明顯,會‘Stop The World’。也就是會把我們的程序暫停。
但是單線程也意味着它非常簡單高效,沒有多餘的線程交互,專心收垃圾就可以了。所以在client版本的java中是默認的新生代收集器。
查看java版本:
ParNew 收集器
Serial收集器的多線程版本,除了使用多線程進行垃圾收集之外。其他的行爲和Serial一樣。
ParNew收集器是server版本的虛擬機中首選的新生代收集器。因爲除了Serial就他可以和CMS配合。
Parallel Scavenge收集器
同樣是新生代的收集器,也同樣是使用複製算法的,並行的多線程收集器。而它與ParNew等其他收集器差異化的地方在於,它的關注點在控制吞吐量,也就是cpu用於運行用戶代碼事件於cpu總消耗時間的比值。所以吞吐量=運行用戶代碼時間/(運行用戶代碼時間+垃圾收集時間)。虛擬機總共運行100分鐘,其中垃圾回收花掉1分鐘,則吞吐量爲 99/99+1 = 99%。
而吞吐量越高表示垃圾回收時間佔比越小,cpu利用效率越高。
所以這個收集器也被稱爲”吞吐量收集器”. 高吞吐量爲目標,即減少垃圾收集時間,讓用戶代碼獲得更長的運行時間;
Serial Old收集器
很重要的一點看名字。老年代版本的串行收集器,使用標記整理算法。
Parallel Old收集器
還是看名字。多線程採集,標記整理算法。
CMS 收集器
Concurrent Mark Sweep收集器是一種以獲得最短回收停頓事件爲目標的收集器,也稱爲併發低停頓收集器或低延遲垃圾收集器;。從名字也能看出使用的是標記清除算法
比前幾個收集器複雜很多,可以分爲4個步驟:
(A)、初始標記(CMS initial mark)
僅標記一下GC Roots能直接關聯到的對象,速度很快;但需要"Stop The World";
(B)、併發標記(CMS concurrent mark)
進行GC Roots 追蹤的過程;剛纔產生的集合中標記出存活對象;
應用程序也在運行;並不能保證可以標記出所有的存活對象;
(C)、重新標記(CMS remark)
爲了修正併發標記期間因用戶程序繼續運作而導致標記變動的那一部分對象的標記記錄;
需要"Stop The World",且停頓時間比初始標記稍長,但遠比並發標記短;
採用多線程並行執行來提升效率;
(D)、併發清除(CMS concurrent sweep)
回收所有的垃圾對象;
由於整個過程中耗時最長的併發標記和併發清除過程收集器線程都可以與用戶線程一起工作,所以,從總體上來說,CMS收集器的內存回收過程是與用戶線程一起併發執行的。
CMS是一款優秀的收集器,它的主要優點在名字上已經體現出來了:並發收集、低停頓。
但是它的缺點在於:
- 造成CPU資源緊張:
從圖中可以看到會比其他收集器多開線程
- 無法處理浮動垃圾
由於CMS併發清理階段用戶線程還在運行着,伴隨程序運行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。
因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程序運作使用。
要是CMS運行期間預留的內存無法滿足程序需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。
- 大量內存碎片
來源“標記—清除”算法。
G1收集器
Garbage-First收集器是當今收集器技術發展最前沿的成果之一,是一款面向服務端應用的垃圾收集器。
圖中可以看到和 CMS差不多,但是G1的採集範圍是整個堆(新生代老生代)。他把內存堆分成多個大小相等的獨立區域,在最後的篩選回收的時候根據這些區域的回收價值和成本決定是否回收掉內存。
Java 虛擬機是一個規範,任何實現該規範的虛擬機都可以用來執行 Java 代碼。android就是覺得現在使用的jvm用着不爽,由於 Androd 運行在移動設備上,內存以及電量等諸多方面跟一般的 PC 設備都有本質的區別 ,一般的 JVM 沒法滿足移動設備的要求,所以自己根據這個規範開發了一個Dalvik 虛擬機。
Dalvik虛擬機主要使用標記清除算法,也可以選擇使用拷貝算法。這取決於編譯時期:
http://androidxref.com/4.4_r1/xref/dalvik/vm/Dvm.mk
ART 是在 Android 4.4 中引入的一個開發者選項,也是 Android 5.0 及更高版本的默認 Android 運行時。google已不再繼續維護和提供 Dalvik 運行時,現在 ART 採用了其字節碼格式。
ART 有多個不同的 GC 方案,這些方案包括運行不同垃圾回收器。默認方案是 CMS。
https://source.android.com/devices/tech/dalvik/gc-debug?hl=zh-cn
這就是內存抖動爲什麼會造成 Android App OOM。
檢測優化內存抖動
內存抖動在Android Profile中表現爲:
內存抖動的問題我們可以通過Alloctions Tracker來進行排查。
在Android Studio中點擊memory profiler中的紅點錄製一段時間的內存申請情況,再點擊結束。
對於基於內存抖動,我們主要需要注意
儘量避免在循環體或者頻繁調用的函數內創建對象,應該把對象創建移到循環體外。
另外還有一個經典的string拼接創建大量小的對象造成的內存抖動。
有時會發現頻繁的調用Log打印日誌,app會變卡頓。
Log.i(TAG,width+”x”+height);
這裏會產生2個新對象,width+”x”與width+”x”+height。
而TAG與x是編譯時就存在的字符串常量池,所以不算新對像。
所以一般來說我們會對日誌輸出Log進行控制,或者使用StringBuilder進行優化。
OOM
OOM就是申請的內存超過了Heap的最大值。
OOM的產生不一定是一次申請的內存就超過了最大值,導致oom的原因基本上都是一般情況,我們的不良代碼平時”積累”下來的。
我們知道Android應用的進程都是從一個叫做Zygote的進程fork出來的。並且每個應用android會對其進行內存限制。我們可以查看
查看/system/build.prop中的對應字段來查看我們app的最大允許申請內存。
-dalvik.vm.heapstartsize
堆分配的初始大小
-dalvik.vm.heapgrowthlimit
正常情況下dvm heap的大小是不會超過dalvik.vm.heapgrowthlimit的值。
-dalvik.vm.heapsize
manifest中指定android:largeHeap爲true的極限堆大小,這個就是堆的最大值。