說(shuō)明
本文是學(xué)習(xí)內(nèi)存優(yōu)化時(shí)個(gè)人的總結(jié)如捅,由于本人是剛開始接觸Android的性能優(yōu)化方面的知識(shí)秆吵,肯定有很多知識(shí)點(diǎn)上的不足和錯(cuò)漏虎囚,請(qǐng)各位諒解角塑。
App內(nèi)存組成以及限制
Android 給每個(gè) App 分配一個(gè) VM ,讓App運(yùn)行在 dalvik 上淘讥,這樣即使 App 崩潰也不會(huì)影響到系統(tǒng)圃伶。系統(tǒng)給 VM 分配了一定的內(nèi)存大小, App 可以申請(qǐng)使用的內(nèi)存大小不能超過(guò)此硬性邏輯限制,就算物理內(nèi)存富余窒朋,如果應(yīng)用超出 VM 最大內(nèi)存搀罢,就會(huì)出現(xiàn)內(nèi)存溢出 crash 。由程序控制操作的內(nèi)存空間在 heap 上侥猩,分 java heapsize 和 native heapsize
Java申請(qǐng)的內(nèi)存在 vm heap 上榔至,所以如果 java 申請(qǐng)的內(nèi)存大小超過(guò) VM 的邏輯內(nèi)存限制,就會(huì)出現(xiàn)內(nèi)存溢出的異常。native層內(nèi)存申請(qǐng)不受其限制, native 層受 native process 對(duì)內(nèi)存大小的限制欺劳。那么如何查看系統(tǒng)對(duì)APP的內(nèi)存限制呢唧取?
(1)如果你的手機(jī)root過(guò),我們可以通過(guò) adb shell 在 命令行窗口查看划提,命令如下:
adb shell cat /system/build.prop
這里主要關(guān)注三個(gè)屬性即可:
1.heapstartsize:App啟動(dòng)的初始分配內(nèi)存
2.heapgrowthlimit:APP能夠分配到的最大限制
3.heapsize:開啟largeHeap=‘true’的最大限制
作為應(yīng)用的開發(fā)者枫弟,這幾個(gè)值我們是無(wú)法改變的(root過(guò)或者手機(jī)系統(tǒng)開發(fā)者除外),我呢只需要知道有這么幾個(gè)值即可鹏往。
(2)通過(guò)代碼獲取
ActivityManager activityManager =(ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)
activityManager.getMemoryClass();//以m為單位
Android內(nèi)存分配與回收機(jī)制
內(nèi)存分配
Android的Heap空間是一個(gè) Generational Heap Memory 的模型淡诗,最近分配的對(duì)象會(huì)存放在 Young
Generation 區(qū)域,當(dāng)一個(gè)對(duì)象在這個(gè)區(qū)域停留的時(shí)間達(dá)到一定程度伊履,它會(huì)被移動(dòng)到 Old
Generation 韩容,最后累積一定時(shí)間再移動(dòng)到 Permanent Generation 區(qū)域。
1湾碎、Young Generation(新生代)
由一個(gè)Eden區(qū)和兩個(gè)Survivor區(qū)組成宙攻,程序中生成的大部分新的對(duì)象都在Eden區(qū)中,當(dāng)Eden區(qū)滿時(shí)介褥,還存活的對(duì)象將被復(fù)制到其中一個(gè)Survivor區(qū),當(dāng)次Survivor區(qū)滿時(shí)递惋,此區(qū)存活的對(duì)象又被復(fù)制到另一個(gè)Survivor區(qū)柔滔,當(dāng)這個(gè)Survivor區(qū)也滿時(shí),會(huì)將其中存活的對(duì)象復(fù)制到年老代萍虽。
2睛廊、Old Generation(老年代)
一般情況下,年老代中的對(duì)象生命周期都比較長(zhǎng)杉编。
3超全、Permanent Generation(持久代)
用于存放靜態(tài)的類和方法,持久代對(duì)垃圾回收沒(méi)有顯著影響邓馒。
總結(jié):內(nèi)存對(duì)象的處理過(guò)程如下:
1嘶朱、對(duì)象創(chuàng)建后在Eden區(qū)。
2光酣、執(zhí)行GC后疏遏,如果對(duì)象仍然存活,則復(fù)制到S0區(qū)。
3财异、當(dāng)S0區(qū)滿時(shí)倘零,該區(qū)域存活對(duì)象將復(fù)制到S1區(qū),然后S0清空戳寸,接下來(lái)S0和S1角色互換呈驶。
4、當(dāng)?shù)?步達(dá)到一定次數(shù)(系統(tǒng)版本不同會(huì)有差異)后疫鹊,存活對(duì)象將被復(fù)制到Old Generation俐东。
5、當(dāng)這個(gè)對(duì)象在Old Generation區(qū)域停留的時(shí)間達(dá)到一定程度時(shí)订晌,它會(huì)被移動(dòng)到Old
Generation虏辫,最后累積一定時(shí)間再移動(dòng)到Permanent Generation區(qū)域。
系統(tǒng)在Young Generation锈拨、Old Generation上采用不同的回收機(jī)制砌庄。每一個(gè)Generation的內(nèi)存區(qū)域都
有固定的大小。隨著新的對(duì)象陸續(xù)被分配到此區(qū)域奕枢,當(dāng)對(duì)象總的大小臨近這一級(jí)別內(nèi)存區(qū)域的閾值時(shí)娄昆,
會(huì)觸發(fā)GC操作,以便騰出空間來(lái)存放其他新的對(duì)象缝彬。
執(zhí)行GC占用的時(shí)間與Generation和Generation中的對(duì)象數(shù)量有關(guān):
Young Generation < Old Generation < Permanent Generation
Gener中的對(duì)象數(shù)量與執(zhí)行時(shí)間成正比萌焰。
4、Young Generation GC
由于其對(duì)象存活時(shí)間短谷浅,因此基于Copying算法(掃描出存活的對(duì)象扒俯,并復(fù)制到一塊新的完全未使用的控件中)來(lái)回收。新生代采用空閑指針的方式來(lái)控制GC觸發(fā)一疯,指針保持最后一個(gè)分配的對(duì)象在Young Generation區(qū)間的位置撼玄,當(dāng)有新的對(duì)象要分配內(nèi)存時(shí),用于檢查空間是否足夠墩邀,不夠就觸發(fā)GC掌猛。
5、Old Generation GC
由于其對(duì)象存活時(shí)間較長(zhǎng)眉睹,比較穩(wěn)定荔茬,因此采用Mark(標(biāo)記)算法(掃描出存活的對(duì)象,然后再回收未被標(biāo)記的對(duì)象竹海,回收后對(duì)空出的空間要么合并慕蔚,要么標(biāo)記出來(lái)便于下次分配,以減少內(nèi)存碎片帶來(lái)的效率損耗)來(lái)回收站削。
可回收對(duì)象的判定
可達(dá)性算法:
從GC Roots(每種具體實(shí)現(xiàn)對(duì)GC Roots有不同的定義)作為起點(diǎn)坊萝,向下搜索它們引用的對(duì)象,可以生成一棵引用樹,樹的節(jié)點(diǎn)視為可達(dá)對(duì)象十偶,反之視為不可達(dá)菩鲜。
Java定義的GC Roots對(duì)象:
虛擬機(jī)棧(幀棧中的本地變量表)中引用的對(duì)象。
方法區(qū)中靜態(tài)屬性引用的對(duì)象惦积。
方法區(qū)中常量引用的對(duì)象接校。
本地方法棧中JNI引用的對(duì)象。
GC類型
kGcCauseForAlloc:分配內(nèi)存不夠引起的GC狮崩,會(huì)Stop World蛛勉。由于是并發(fā)GC,其它線程都會(huì)停止睦柴,直到GC完成诽凌。
kGcCauseBackground:內(nèi)存達(dá)到一定閾值觸發(fā)的GC,由于是一個(gè)后臺(tái)GC坦敌,所以不會(huì)引起Stop World侣诵。
kGcCauseExplicit:顯示調(diào)用時(shí)進(jìn)行的GC,當(dāng)ART打開這個(gè)選項(xiàng)時(shí)狱窘,使用System.gc時(shí)會(huì)進(jìn)行GC杜顺。
GC算法
1.標(biāo)記清除算法
分為兩步
標(biāo)價(jià): 標(biāo)記的過(guò)程其實(shí)就是,遍歷所有的GC Roots蘸炸,然后將所有的 GC Roots可達(dá)的對(duì)象標(biāo)記為存活的對(duì)象躬络。
清除:清除的過(guò)程將遍歷堆中所有的對(duì)象中沒(méi)有標(biāo)記的對(duì)象全部清除掉
特點(diǎn):
(1)掃描兩次
(2)位置不連續(xù),存在碎片
(3)兩遍掃描
2.復(fù)制算法
描述:
(1)復(fù)制算法將內(nèi)存劃分為兩個(gè)區(qū)間搭儒,在任意時(shí)間點(diǎn)穷当,所有動(dòng)態(tài)分配的對(duì)象都只能分配在其中一個(gè)區(qū)間(稱為活動(dòng)區(qū)間),而另外一個(gè)區(qū)間(稱為空閑區(qū)間)則是空閑的仗嗦。
(2)當(dāng)有效內(nèi)存空間耗盡時(shí)膘滨,JVM將暫停程序運(yùn)行,開啟復(fù)制算法GC線程稀拐。接下來(lái)GC線程會(huì)將活動(dòng)區(qū)間內(nèi)的存活對(duì)象,全部復(fù)制到空閑區(qū)間丹弱,且嚴(yán)格按照內(nèi)存地址依次排列德撬,與此同時(shí),GC線程將更新存活對(duì)象的內(nèi)存引用地址指向新的內(nèi)存地址躲胳。
(3)此時(shí)蜓洪,空閑區(qū)間已經(jīng)與活動(dòng)區(qū)間交換,而垃圾對(duì)象現(xiàn)在已經(jīng)全部留在了原來(lái)的活動(dòng)區(qū)間坯苹,也就是現(xiàn)在的空閑區(qū)間隆檀。事實(shí)上,在活動(dòng)區(qū)間轉(zhuǎn)換為空間區(qū)間的同時(shí),垃圾對(duì)象已經(jīng)被一次性全部回收恐仑。
特點(diǎn):
(1)實(shí)現(xiàn)簡(jiǎn)單泉坐,運(yùn)行高效
(2)空間利用率只有一半
(3)沒(méi)有碎片
3.標(biāo)記整理算法
描述:
y與標(biāo)記/清除算法類似,分為兩步
(1)標(biāo)記:它的第一個(gè)階段與標(biāo)記/清除算法是一模一樣的裳仆,均是遍歷GC Roots腕让,然后將存活的對(duì)象標(biāo)記。
(2)整理:移動(dòng)所有存活的對(duì)象歧斟,且按照內(nèi)存地址次序依次排列纯丸,然后將末端內(nèi)存地址以后的內(nèi)存全部回收。因此静袖,第二階段才稱為整理階段觉鼻。
特點(diǎn):
(1)沒(méi)有內(nèi)存碎片
(2)效率偏低
(3)兩遍掃描,指針需要移動(dòng)
Android低內(nèi)存殺進(jìn)程機(jī)制
Anroid基于進(jìn)程中運(yùn)行的組件及其狀態(tài)規(guī)定了默認(rèn)的五個(gè)回收優(yōu)先級(jí):
Empty process(空進(jìn)程)
Background process(后臺(tái)進(jìn)程)
Service process(服務(wù)進(jìn)程)
Visible process(可見(jiàn)進(jìn)程)
Foreground process(前臺(tái)進(jìn)程)
系統(tǒng)需要進(jìn)行內(nèi)存回收時(shí)最先回收空進(jìn)程,然后是后臺(tái)進(jìn)程队橙,以此類推最后才會(huì)回收前臺(tái)進(jìn)程(一般情況
下前臺(tái)進(jìn)程就是與用戶交互的進(jìn)程了,如果連前臺(tái)進(jìn)程都需要回收那么此時(shí)系統(tǒng)幾乎不可用了)坠陈。
ActivityManagerService 會(huì)對(duì)所有進(jìn)程進(jìn)行評(píng)分(存放在變量adj中),然后再講這個(gè)評(píng)分更新到內(nèi)核喘帚,由內(nèi)核去完成真正的內(nèi)存回收( lowmemorykiller , Oom_killer )畅姊。這里只是大概的流程,中間過(guò)程還是很復(fù)雜的
什么是OOM
OOM(OutOfMemoryError)內(nèi)存溢出錯(cuò)誤吹由,在常見(jiàn)的Crash疑難排行榜上若未,OOM絕對(duì)可以名列前茅并且經(jīng)久不衰。因?yàn)樗l(fā)生時(shí)的Crash堆棧信息往往不是導(dǎo)致問(wèn)題的根本原因倾鲫,而只是壓死駱駝的最后一根稻草粗合。
OOM分類
內(nèi)存泄露的解決方法
1.常見(jiàn)的分析工具
(1)MAT
(2)Memory Profile
(3)LeakCanary
Memory Profile檢測(cè)內(nèi)存泄露
首先我們寫一個(gè)測(cè)試Demo,在MainActivity中打開SecondActivity乌昔,在伴隨對(duì)象中持有SeconnActivity的實(shí)例隙疚,然后關(guān)閉SecondActivity的實(shí)例。secndActivity的到代碼如下:
class SecondActivity : AppCompatActivity() {
private lateinit var mButton: AppCompatButton
private var mWeakRef: WeakReference<String>? = null
companion object {
var context123: Context? = null
var weakReferenceObj: WeakReference<String>? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
context123 = this
setContentView(R.layout.activity_second)
mButton = findViewById(R.id.btn_click)
mWeakRef = WeakReference("lala")
weakReferenceObj = mWeakRef
mButton.setOnClickListener {
finish()
}
}
}
首先我們用Memory Proflile 分析該內(nèi)存泄露
步驟如下
(1)運(yùn)行項(xiàng)目
(2)點(diǎn)擊Profile進(jìn)入分析界面磕道,點(diǎn)擊左上角的+添加分析的項(xiàng)目
(3)綁定成功回進(jìn)入分析界面供屉,點(diǎn)擊memory,進(jìn)入內(nèi)存分析
(4)打開SecondActivity頁(yè)面后在關(guān)閉該頁(yè)面溺蕉,點(diǎn)擊Capture head dump伶丐,再按下record捕捉內(nèi)存視圖
(5)選擇show activity/fragment Leaks 既可以看到發(fā)生內(nèi)存泄露的相關(guān)activity或fragment
(6)點(diǎn)擊下方的Instance List的相關(guān)實(shí)例,點(diǎn)擊Reference便可以看到相關(guān)對(duì)象的持有情況疯特。
(7)分析相關(guān)的引用持有情況哗魂,這里可以看到,我們自己寫的mContext123漓雅,分析該mContext何時(shí)被賦值的
companion object {
var context123: Context? = null
var weakReferenceObj: WeakReference<String>? = null
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
context123 = this
setContentView(R.layout.activity_second)
mButton = findViewById(R.id.btn_click)
mWeakRef = WeakReference("lala")
weakReferenceObj = mWeakRef
mButton.setOnClickListener {
finish()
}
}
發(fā)現(xiàn)該mContext123持有了一個(gè)SecondActivity的實(shí)例录别,當(dāng)該SecondActivity對(duì)象想要執(zhí)行銷毀時(shí)朽色,因?yàn)楸籱Context123持有而無(wú)法被銷毀,從而造成了內(nèi)存泄露组题。
至此Meomery Profile的內(nèi)存泄露檢測(cè)說(shuō)明完畢
LeakCanary檢測(cè)內(nèi)存泄露
(1)首先引入LeakCanary
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'
(2)然后運(yùn)行項(xiàng)目葫男,LeakCanary會(huì)自動(dòng)檢測(cè)內(nèi)存泄露的點(diǎn),如果檢測(cè)到會(huì)在通知欄顯示一條通知
(3)點(diǎn)擊后進(jìn)入Leack Canary 可以看到發(fā)生了泄露
(4)點(diǎn)擊該條目往踢,可以看到發(fā)生泄露的點(diǎn)腾誉,可以看到與Memory Profile找的泄漏點(diǎn)一致。
至此峻呕,LeakCanary檢測(cè)內(nèi)存泄露的說(shuō)明講解完畢