對(duì)于一個(gè)android開(kāi)發(fā)來(lái)說(shuō),內(nèi)存優(yōu)化是一個(gè)亙古不變的話題珍手。很多東西看過(guò)了當(dāng)時(shí)理解了准夷,但是過(guò)一段時(shí)間會(huì)忘,今天就系統(tǒng)的記錄一下豪嗽。
先畫(huà)個(gè)思維導(dǎo)圖谴蔑,免得寫(xiě)起來(lái)沒(méi)有章法豌骏。
想要做內(nèi)存優(yōu)化,首先必須得jvm的內(nèi)存模型有一定的了解隐锭。
JVM內(nèi)存模型
java的內(nèi)存管理是交給GC(Garbage Collection)來(lái)處理的,GC會(huì)幫助我們回收不再使用的內(nèi)存資源窃躲。但是也有些時(shí)候由于程序員的疏忽大意,導(dǎo)致GC無(wú)法回收,此時(shí)便造成了內(nèi)存泄漏.
懶得畫(huà)了,借用網(wǎng)上的圖片大致說(shuō)明下
jvm運(yùn)行時(shí)模型包括
1.方法區(qū)(Method Area)
2.堆(Heap)
3.虛擬機(jī)棧(VM Stack也叫JavaStack)
4.本地方法棧(Native Method Stack)
5.程序計(jì)數(shù)器(Program Counter Register)
1和2也就是方法區(qū)和堆是線程間共享的,而345是線程私有的!
那么為什么堆和方法區(qū)是線程間共享的而其他區(qū)域卻是線程間隔離的呢?
首先我們的理解方法區(qū)和堆都負(fù)責(zé)存放哪些信息
-
堆 (Heap)
用于存儲(chǔ)對(duì)象實(shí)例,內(nèi)存不連續(xù),生命周期不確定,是GC重點(diǎn)回收區(qū)域,當(dāng)內(nèi)存不足時(shí)會(huì)觸發(fā)OutOfMemory異常 -
方法區(qū)(Method Area)
主要負(fù)責(zé)存儲(chǔ)被虛擬機(jī)加載的類信息蒂窒、常量、靜態(tài)變量衰抑、即時(shí)編譯器編譯后的代碼等數(shù)據(jù),當(dāng)內(nèi)存不足時(shí)同樣會(huì)觸發(fā)OutOfMemory一樣
-
虛擬機(jī)棧(JavaStack)
存儲(chǔ)局部變量表、操作棧采盒、動(dòng)態(tài)鏈接磅氨、方法出口等信息,Java每個(gè)方法被執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀烦租,并壓入JavaStack中
畫(huà)張圖幫助理解
那么我現(xiàn)在知道Java棧中存放局部變量等信息了
此時(shí)我們來(lái)思考上面的問(wèn)題(為什么java棧要線程隔離呢?)
試想下如果在并發(fā)多線程時(shí),如果A線程可以訪問(wèn)B線程中調(diào)用的方法中的局部變量,那么程序是不是就亂了套了
棧可能觸發(fā)兩種異常
1.在單線程時(shí),如果線程申請(qǐng)的棧深度超出了虛擬機(jī)允許的最大深度時(shí),會(huì)拋出StackOverflow異常!比如遞歸遍歷的時(shí)候不斷壓棧就可能會(huì)爬出StackOverflow異常
2.虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展,在多線程時(shí),當(dāng)棿蠡牵空間超出了虛擬機(jī)分配的大小時(shí),此時(shí)當(dāng)線程申請(qǐng)開(kāi)辟棧空間時(shí)會(huì)觸發(fā)OutOfMemory異常
-
本地方法棧(Native Heap)
不做過(guò)多講解,其實(shí)作用和java棧一樣,只不過(guò)存儲(chǔ)的是c/c++等JNI調(diào)用的方法中的信息 -
程序計(jì)數(shù)器(Program Counter Register)
存儲(chǔ)正在執(zhí)行的虛擬機(jī)字節(jié)碼的指令的地址
如果當(dāng)前線程正在執(zhí)行的是一個(gè)Java方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是一個(gè)Native方法绑嘹,這個(gè)計(jì)數(shù)器值null稽荧。
那么程序計(jì)數(shù)器為什么是線程間私有呢?
我們知道java并發(fā)多線程時(shí),其實(shí)時(shí)輪換執(zhí)行,其實(shí)同一時(shí)間只有一個(gè)線程能夠得到執(zhí)行!
假如A線程執(zhí)行到M方法的第8行此時(shí)切換到B線程執(zhí)行, 此時(shí)程序計(jì)數(shù)器會(huì)記錄下M方法第8行的字節(jié)碼指令地址,等到再次輪換到A線程執(zhí)行的時(shí)候,就接著從第8行往下執(zhí)行工腋。設(shè)計(jì)成線程間私有,每個(gè)線程擁有一個(gè)自己的程序計(jì)數(shù)器,彼此之間不會(huì)干擾!
什么是內(nèi)存泄漏,什么情況下會(huì)發(fā)生內(nèi)存泄漏?
上面說(shuō)了,java/kotlin new出來(lái)的對(duì)象放在 "堆"中的,那么GC是如何判斷一個(gè)對(duì)象是否應(yīng)該被回收了呢?
判斷java對(duì)象是否該被回收通常有兩種方式
1.引用計(jì)數(shù)
比如A被B引用,計(jì)數(shù)加一,A又被C引用計(jì)數(shù)加一,B對(duì)象釋放了,那么A的引用計(jì)數(shù)就減一。當(dāng)引用計(jì)數(shù)為0時(shí)擅腰,則該對(duì)象可以被回收!但是無(wú)法解決循環(huán)引用的問(wèn)題
2.GCRoot可達(dá)性
從一個(gè)對(duì)象不斷的向上追溯引用,如果無(wú)法追溯到GCRoot的時(shí)候,那么這個(gè)對(duì)象就可以被回收了!
比方說(shuō)A引用B,B又引用A,但是此時(shí)A和B都沒(méi)有被其他對(duì)象引用,那么A和B其實(shí)是已經(jīng)沒(méi)有用的垃圾對(duì)象了,但是他們的引用計(jì)數(shù)并不為0,那么此時(shí)就得通過(guò)GCRoot可達(dá)性來(lái)判斷它是否可以被回收了
上圖吧,文字解釋太蒼白無(wú)力
GC Root有哪些?
- 虛擬機(jī)中引用的對(duì)象(局部變量引用的對(duì)象)
- 方法區(qū)中類靜態(tài)屬性引用的對(duì)象
- 方法區(qū)中常量引用對(duì)象
- 本地方法棧中JNI引用對(duì)象
- 活著的線程
那么現(xiàn)在來(lái)想想什么是內(nèi)存泄漏呢?
通俗點(diǎn)來(lái)說(shuō)就是,某個(gè)對(duì)象明明已經(jīng)沒(méi)有用了,但是它卻可達(dá)GCRoot,那么這時(shí)候,GC就無(wú)法對(duì)這個(gè)沒(méi)有用的對(duì)象進(jìn)行回收,就導(dǎo)致了內(nèi)存泄漏趁冈。
什么情況下會(huì)發(fā)生內(nèi)存泄漏呢?
舉兩個(gè)例子
我們知道內(nèi)部類會(huì)持有外部類的引用,當(dāng)我們使用Handler時(shí),如果MyHandler不是靜態(tài)內(nèi)部類,那么可以看到AS會(huì)發(fā)出一個(gè)警告!加入TestActivity已經(jīng)關(guān)閉了,但是此時(shí)MyHandler的MessageQueue中還有未被執(zhí)行的Message,那么MyHandler并不會(huì)被銷毀,而且它還持有者TestActivity的引用,那么此時(shí)就造成了TestActivity的內(nèi)存泄漏
解決辦法
要么將MyHandler聲明為static的,要么將TestActivity使用WeakReference包裹起來(lái)!
Singleton.java
package com.taylor.androidinterview;
import android.content.Context;
/**
* Copyright:AndroidInterview
* Author: liyang <br>
* Date:2019-10-07 20:19<br>
* Desc: <br>
*/
public class Singleton {
private static Singleton sInstance;
private static Context mContext;
private Singleton(Context context) {
mContext = context;
}
public static Singleton getInstance(Context context) {
if (sInstance == null) {
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton(context);
}
}
}
return sInstance;
}
}
TestActivity.java
package com.taylor.androidinterview;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import androidx.appcompat.app.AppCompatActivity;
public class TestActivity extends AppCompatActivity {
private MyHandler myHandler;
private Singleton mSingleton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
mSingleton = Singleton.getInstance(this);
myHandler=new MyHandler();
myHandler.postDelayed(() -> System.out.println("我被執(zhí)行了!!!"),100000000);
findViewById(R.id.btnClose).setOnClickListener(v-> finish());
}
class MyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
}
}
SIngleton是個(gè)單例類,他的內(nèi)部的mContext變量是static的,所以mContext是一個(gè)GCRoot,那么此時(shí)當(dāng)TestActivity關(guān)閉時(shí),由于Singleton的mContext是GCRoot并且持有了TestActivity,那么TestActivity并不能得到回收,同樣造成了內(nèi)存泄漏.
解決辦法,使用ApplicationContext代替Context,因?yàn)锳pplicationContext是和App生命周期一致的
其實(shí)AS已經(jīng)很強(qiáng)大了,如果你的代碼可能會(huì)導(dǎo)致內(nèi)存泄漏,大部分情況下AS都會(huì)給出警告的,只是很多人忽視警告而已
當(dāng)然android中可能導(dǎo)致內(nèi)存泄漏的情況還有很多,比如Activity銷毀了但是沒(méi)有解除監(jiān)聽(tīng),Activity銷毀了但是還有活著的線程等等
Android內(nèi)存泄漏如何檢測(cè)?
- adb觀察法
- LeakCanary等三方檢測(cè)庫(kù)
- 使用Profiler分析檢測(cè)
- 使用MAT進(jìn)行檢測(cè)
adb觀察法
我在MainActivity中打開(kāi)了剛才寫(xiě)的TestActivity,然后就關(guān)閉了TestActivity,為了避免JVM沒(méi)有及時(shí)回收TestActivity,我又使用Profiler主動(dòng)進(jìn)行了一個(gè)垃圾回收!
此時(shí)我們使用adb shell dunpsys meminfo com.taylor.androidinterview -d
命令查看下app的內(nèi)存情況
Objects:
中羅列的就是當(dāng)前app中現(xiàn)在存活的實(shí)例
Views有36個(gè),AppContext有4個(gè),Activity有兩個(gè)
我們明明關(guān)閉了TestActivity,此時(shí)app中應(yīng)該只有一個(gè)Activity,但是由于TestActivity中使用Singleton和MyHandler導(dǎo)致了TestActivity內(nèi)存泄漏了
adb觀察法只能粗略的查看下內(nèi)存情況,如果想要追溯內(nèi)存泄漏的具體位置就得借助LeakCanary、Profiler和MAT這些工具了
LeakCanary 就不做過(guò)多介紹了
具體可以查看官方文檔http://square.github.io/leakcanary
,其實(shí)LeakCanary的原理和我們接下來(lái)要講的使用Profiler和MAT分析差不多,但是即便是使用LeakCanary還是會(huì)有一些情況它無(wú)法檢測(cè)到
LeakCanary原理:
LeakCanary會(huì)在Activity的onDestroy方法中取刃,主動(dòng)調(diào)用 GC坯辩,并利用ReferenceQueue+WeakReference馁龟,來(lái)判斷是否有釋放不掉的引用,然后結(jié)合dump memory的hpof文件, 用HaHa分析出泄漏地方濒翻。
使用Profiler分析檢測(cè)
接下來(lái)我們使用Profiler來(lái)進(jìn)行分析
我又反復(fù)打開(kāi)TestActivity然后又關(guān)閉掉兩次,
可以看到我選取的app內(nèi)存信息的時(shí)間點(diǎn)是在TestActivity銷毀之后,但是此時(shí)可以看到App中任然有3個(gè)TestActivity實(shí)例
Profiler的功能非常強(qiáng)大,你可以用它來(lái)分析網(wǎng)絡(luò),分析性能,分析內(nèi)存情況等,絕對(duì)是分析查找問(wèn)題的好幫手
profiler的具體使用請(qǐng)查看google的官方文檔https://developer.android.google.cn/studio/profile/memory-profiler
使用MAT進(jìn)行檢測(cè)分析
MAT是Memory Analysis Tools的縮寫(xiě),從名稱就可以看出他是專門(mén)用來(lái)分析內(nèi)存的工具
- 第一步:接下來(lái)我們使用profiler dump內(nèi)存快照并保存為
memory-mat.hprof
MAT可以解析Java SE HPROF 格式的.hprof文件,但是無(wú)法解析android格式的.hprof文件,所以我們需要android_sdk/platform-tools/ hrpof-conv
工具將.hprof文件轉(zhuǎn)換為MAT可以分析的格式
如果你沒(méi)有將hprof-conv工具配置到全局變量里那么就需要到它所在的文件夾內(nèi)才能使用hprof-conv命令
- 第二步 轉(zhuǎn)換hprof文件格式
hprof-conv <原h(huán)prof文件名 ><轉(zhuǎn)換后的hprof文件名>
liyangdeMacBook-Pro:~ liyang$ cd /Users/liyang/Library/Android/sdk/platform-tools
liyangdeMacBook-Pro:platform-tools liyang$ hprof-conv /Users/liyang/Downloads/work/AndroidInterview/memory-mat.hprof /Users/liyang/Downloads/work/AndroidInterview/memory-mat-conv.hprof
接下來(lái)我們就可以使用MAT對(duì)堆存快照進(jìn)行分析,精確內(nèi)存泄漏的位置了
-
第三步 使用MAT打開(kāi)轉(zhuǎn)換后的memory-mat-conv.hprof文件
打開(kāi)后,我們先點(diǎn)擊Overview概覽這個(gè)tab
Overview界面的下方actions這一列中有四個(gè)選項(xiàng)
功能名 | 作用 |
---|---|
histogram | 羅列出每個(gè)class類中的實(shí)例個(gè)數(shù)等情況(我們分析內(nèi)存泄漏使用的就是這個(gè)功能) |
DominatorTree | 羅列出那些一直存活的占用內(nèi)存較大的對(duì)象(如果你的app發(fā)生的OutOfMemory可以使用這個(gè)功能進(jìn)行分析,從那些過(guò)于龐大的對(duì)象中找出元兇) |
Top Consumer | 可以根據(jù)class或者package進(jìn)行分組,打印出那些最耗費(fèi)性能的對(duì)象(如果你覺(jué)得你的項(xiàng)目運(yùn)行時(shí)比較卡頓,可以使用這個(gè)功能進(jìn)行分析查找) |
Duplicate Classes | 檢測(cè)被多個(gè)ClassLoader加載的類 |
- 第四步 接下來(lái)我們使用Histogram功能查看下我們的app中的實(shí)例對(duì)象情況
打開(kāi)Histogram后,它默認(rèn)是group by class,我們選擇group by package 找到我們的app
通過(guò)按照package來(lái)分組,我們可以很快找到自己的app,從上圖可以看到,何Profiler中的情況一樣TestActivity仍然有3個(gè)實(shí)例,也就是我們每打開(kāi)一次TestActivity都會(huì)有一個(gè)實(shí)例內(nèi)存泄漏了
上圖中被我圈住的Shallow Heap和Retained Heap是代表什么意思呢?
名稱 | 代表什么意思呢 |
---|---|
Shallow Heap | 對(duì)象本身占用內(nèi)存的大小屁柏,不包含其引用的對(duì)象。(單位是字節(jié)) |
Retained Heap | 是對(duì)象及被其引用的對(duì)象的總大小,如果該對(duì)象被釋放了進(jìn)而可以釋放的大小(單位是字節(jié)) |
-
第五步 接下來(lái)我們查看下TestActivity都被誰(shuí)引用而導(dǎo)致泄漏的呢?
選中TestActivity,右鍵可以看到一系列的功能,有List Objects/Show objects by class/...
List Objects和Show objects by class功能差不多
前者是按照實(shí)例分組,比如這里TestActivity有3個(gè)實(shí)例,會(huì)羅列出三個(gè)實(shí)例
后者是按照類來(lái)分組,那么久會(huì)列出TestActivity這一個(gè)類
這里重點(diǎn)應(yīng)該介紹with outgoing reference和with incoming reference
with outgoing reference
看字面意思大概也可以知道是這個(gè)對(duì)象被誰(shuí)引用了
with incoming reference
也就是這個(gè)對(duì)象引用了誰(shuí)
這里因?yàn)門(mén)estActivity內(nèi)存泄漏了,當(dāng)然我們要看看是誰(shuí)持有了它導(dǎo)致它內(nèi)存泄漏的,選outgoing
選擇了outgoing后,根據(jù)上圖可以看到TestActivity被這么多對(duì)象引用了,那么怎么找出元兇呢?
那么我們直接選Path To GC Roots
,然后看到選擇之后會(huì)出現(xiàn)有一些選項(xiàng),排除弱引用, 排除軟引用...
我們知道java中有四種引用類型
引用類型 | 介紹 |
---|---|
強(qiáng)引用(我們直接new出來(lái)的對(duì)象都是強(qiáng)引用) | 強(qiáng)引用有三個(gè)特點(diǎn)(一)強(qiáng)引用可以直接訪問(wèn)目標(biāo)對(duì)象有送。(二)強(qiáng)引用所指向的對(duì)象在任何時(shí)候都不會(huì)被系統(tǒng)回收淌喻。JVM寧愿拋出OOM異常,也不會(huì)回收強(qiáng)引用所指向的對(duì)象雀摘。(三)強(qiáng)引用可能導(dǎo)致內(nèi)存泄漏裸删。 |
軟引用(SoftReference) | 軟引用是除了強(qiáng)引用外,最強(qiáng)的引用類型阵赠⊙乃可以通過(guò)java.lang.ref.SoftReference使用軟引用。一個(gè)持有軟引用的對(duì)象清蚀,不會(huì)被JVM很快回收匕荸,JVM會(huì)根據(jù)當(dāng)前堆的使用情況來(lái)判斷何時(shí)回收。當(dāng)堆使用率臨近閾值時(shí)枷邪,才會(huì)去回收軟引用的對(duì)象榛搔。因此,軟引用可以用于實(shí)現(xiàn)對(duì)內(nèi)存敏感的高速緩存东揣。 |
弱引用(WeakReference) | 弱引用是一種比軟引用較弱的引用類型践惑。弱引用對(duì)象的存在不會(huì)阻止它所指向的對(duì)象變被垃圾回收器回收。在系統(tǒng)GC時(shí)嘶卧,只要發(fā)現(xiàn)弱引用尔觉,不管系統(tǒng)堆空間是否足夠,都會(huì)將對(duì)象進(jìn)行回收芥吟。在java中侦铜,可以用java.lang.ref.WeakReference實(shí)例來(lái)保存對(duì)一個(gè)Java對(duì)象的弱引用。 |
虛引用(PhantomReference) | 虛引用是所有類型中最弱的一個(gè)运沦。一個(gè)持有虛引用的對(duì)象泵额,和沒(méi)有引用幾乎是一樣的,隨時(shí)可能被垃圾回收器回收携添。當(dāng)試圖通過(guò)虛引用的get()方法取得強(qiáng)引用時(shí),總是會(huì)失敗篓叶。并且烈掠,虛引用必須和引用隊(duì)列一起使用羞秤,它的作用在于跟蹤垃圾回收過(guò)程。 |
根據(jù)強(qiáng)度排個(gè)序
強(qiáng)引用>軟引用>弱引用>虛引用
我們過(guò)濾掉軟引用,弱引用和虛引用再看看
選擇了exclude all phatom/weak/soft etc. reference
之后
可以看到,TestActivity被兩個(gè)GC Roots引用了,一個(gè)是Singleton中的mContext變量因?yàn)槭庆o態(tài)的,另一個(gè)就是mHandler,是非靜態(tài)內(nèi)部類
罪魁禍?zhǔn)渍业搅?/strong>
android中如何盡量避免內(nèi)存泄漏呢?
大致可以總結(jié)一些情況
- getSystemService的時(shí)候左敌,應(yīng)避免使用activity的context瘾蛋,而是使用application的context
- 單例模式中盡量避免使用context,如果一定要使用應(yīng)使用context.getApplicationContext來(lái)代替
- 如果使用觀察者模式時(shí),在onCreate或者onStart中注冊(cè)了監(jiān)聽(tīng),要記得在對(duì)應(yīng)的生命周期中解除監(jiān)聽(tīng)
- 非靜態(tài)內(nèi)部類矫限、匿名內(nèi)部類會(huì)持有外部類的實(shí)例引用哺哼,導(dǎo)致泄漏〉鸱纾可以在不需要的時(shí)候主動(dòng)置空或者使用弱引用
- 在Runnable中做操作時(shí)一定要小心,如果Activity或Fragment銷毀了,及時(shí)停止線程
- 使用資源時(shí)(比如Cursor取董、File、Bitmap无宿、視頻茵汰、音頻等)及時(shí)關(guān)閉
- Glide.with(context)這個(gè)context不可以亂用
- ...歡迎補(bǔ)充