前情提要
java中四種引用類型
StrongReference強(qiáng)引用
如 Object o = new Object()
- 回收時(shí)機(jī):從不回收
- 使用:對(duì)象的一般保存
- 生命周期:JVM停止的時(shí)候才會(huì)終止
SoftReference軟引用
- 回收時(shí)機(jī):當(dāng)內(nèi)存不足的時(shí)候咆繁;
- 使用:SoftReference結(jié)合- ReferenceQueue構(gòu)造有效期短;
- 生命周期:內(nèi)存不足時(shí)終止
WeakReference,弱引用
- 回收時(shí)機(jī):在垃圾回收的時(shí)候眼坏;
- 使用:同軟引用;
- 生命周期:GC后終止
PhatomReference 虛引用
- 回收時(shí)機(jī):在垃圾回收的時(shí)候;
- 使用:合ReferenceQueue來(lái)跟蹤對(duì)象唄垃圾回收期回收的活動(dòng);
- 生命周期:GC后終止
Java 程序運(yùn)行時(shí)的內(nèi)存分配
Java 程序運(yùn)行時(shí)的內(nèi)存分配策略有三種:靜態(tài)分配、棧式分配和堆式分配服猪。
對(duì)應(yīng)的存儲(chǔ)區(qū)域如下:
靜態(tài)存儲(chǔ)區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)、全局 static 數(shù)據(jù)和常量拐云。這塊內(nèi)存在程序編譯時(shí)就已經(jīng)分配好罢猪,并且在程序整個(gè)運(yùn)行期間都存在。
棧區(qū) :方法體內(nèi)的局部變量都在棧上創(chuàng)建叉瘩,并在方法執(zhí)行結(jié)束時(shí)這些局部變量所持有的內(nèi)存將會(huì)自動(dòng)被釋放膳帕。
堆區(qū) : 又稱動(dòng)態(tài)內(nèi)存分配,通常就是指在程序運(yùn)行時(shí)直接 new 出來(lái)的內(nèi)存。這部分內(nèi)存在不使用時(shí)將會(huì)由 Java 垃圾回收器來(lái)負(fù)責(zé)回收危彩。
棧和堆的區(qū)別
棧內(nèi)存:在方法體內(nèi)定義的局部變量(一些基本類型的變量和對(duì)象的引用變量)都是在方法的棧內(nèi)存中分配的攒磨。當(dāng)在一段方法塊中定義一個(gè)變量時(shí),Java 就會(huì)在棧中為該變量分配內(nèi)存空間汤徽,當(dāng)超過(guò)該變量的作用域后娩缰,分配給它的內(nèi)存空間也將被釋放掉,該內(nèi)存空間可以被重新使用谒府。
堆內(nèi)存:用來(lái)存放所有由 new 創(chuàng)建的對(duì)象(包括該對(duì)象其中的所有成員變量)和數(shù)組拼坎。在堆中分配的內(nèi)存泰鸡,將由 Java 垃圾回收器來(lái)自動(dòng)管理。在堆中產(chǎn)生了一個(gè)數(shù)組或者對(duì)象后,還可以在棧中定義一個(gè)特殊的變量欧芽,這個(gè)變量的取值等于數(shù)組或者對(duì)象在堆內(nèi)存中的首地址库正,這個(gè)特殊的變量就是我們上面說(shuō)的引用變量。我們可以通過(guò)這個(gè)引用變量來(lái)訪問(wèn)堆中的對(duì)象或者數(shù)組喷楣。
棧內(nèi)存:基本類型變量、對(duì)象引用變量 、方法內(nèi)局部變量
堆內(nèi)存: new 出來(lái)的對(duì)象
看下面代碼
public class A {
int a = 0; // 棧內(nèi)
B b = new B(); // new B()堆內(nèi) b在棧內(nèi)
public void test(){
int a1 = 1; //棧內(nèi)
B b1 = new B(); // b1在棧內(nèi) new B() 在堆內(nèi)
}
}
A object = new A(); //object棧內(nèi) new A() 堆內(nèi)
A類內(nèi)的局部變量都存在于棧中岛蚤,包括基本數(shù)據(jù)類型a1和引用變量b1,b1指向的B對(duì)象實(shí)體存在于堆中
引用變量object存在于棧中,而object指向的對(duì)象實(shí)體存在于堆中。new A 對(duì)象的所有成員變量a和b在棧內(nèi)(句柄),而引用變量b指向的B類對(duì)象實(shí)體存在于堆中。
主線程的Looper對(duì)象的生命周期 = 該應(yīng)用程序的生命周期
在Java中,非靜態(tài)內(nèi)部類 & 匿名內(nèi)部類都默認(rèn)持有 外部類的引用
舉例handler內(nèi)部msg —— handler實(shí)例 ——Activity實(shí)例
造成內(nèi)存泄漏情景
非靜態(tài)內(nèi)部類導(dǎo)致的內(nèi)存泄露群叶,比如Handler埠通,解決方法是將內(nèi)部類寫(xiě)成靜態(tài)內(nèi)部類端辱,在靜態(tài)內(nèi)部類中使用軟引用/弱引用持有外部類的實(shí)例
IO操作后,沒(méi)有關(guān)閉文件導(dǎo)致的內(nèi)存泄露,比如Cursor鸡岗、FileInputStream、FileOutputStream使用完后沒(méi)有關(guān)閉
自定義View中使用TypedArray后,沒(méi)有recycle
Context 造成的內(nèi)存泄漏 如單例模式中的內(nèi)存泄漏卸察。解決方法:使用Application的Context
注冊(cè)監(jiān)聽(tīng)器的泄漏 沒(méi)有在destory時(shí) unregisterxxx()
集合中對(duì)象沒(méi)清理造成的內(nèi)存泄漏 解決方法 :在Activity退出之前涡扼,將集合里的東西clear什猖,然后置為null,再退出程序
WebView造成的泄露
當(dāng)我們不要使用WebView對(duì)象時(shí)遂黍,應(yīng)該調(diào)用它的destory()函數(shù)來(lái)銷毀它俊嗽,并釋放其占用的內(nèi)存雾家,否則其占用的內(nèi)存長(zhǎng)期也不能被回收,從而造成內(nèi)存泄露绍豁。
adb dumpsys meminfo packageName 查找內(nèi)存泄漏
adb shell dumpsys meminfo packagename -d命令芯咧,反復(fù)進(jìn)入、退出同一界面,并對(duì)比兩次的Activity和View的數(shù)量變化敬飒。如果有差異邪铲,則說(shuō)明存在內(nèi)存泄露(在使用命令查看Activity和View的數(shù)量之前,記得手動(dòng)觸發(fā)GC)无拗。
要繼續(xù)觀察dumpsys meminfo 包名带到, 輸出的結(jié)果信息,關(guān)注點(diǎn)放在 UnKnown那一行 和 Native Heap 那一行英染,關(guān)注Heap Alloc 或者 Pss Total, 如果你的總TOTAL一直再增加揽惹,但是是由于這兩行的增加,那么這個(gè)問(wèn)題你不需要再繼續(xù)在MAT上花時(shí)間了四康,因?yàn)檫@種內(nèi)存泄露問(wèn)題搪搏,出在Native層(C)那么你需要去找你程序中使用到JNI的地方,so庫(kù)或者其他一些特殊調(diào)用上闪金,分析它們是否可能造成內(nèi)存泄露問(wèn)題疯溺。
adb shell showmap -a PID
然興許你依舊沒(méi)有頭緒,那么沒(méi)關(guān)系毕泌,另一個(gè)命令就是為了你而存在的喝检,(首先某個(gè)應(yīng)用的PID號(hào), 用dumpsys meminfo 包名,那邊已經(jīng)可以查到)
譬如我上面那個(gè)mms, PID號(hào)為2786撼泛, 接著adb shell showmap -a PID號(hào) (adb shell showmap -a 2786)
然后根據(jù)結(jié)果[....]這的信息挠说,在去google上面找關(guān)鍵字, 譬如:[ anon ] bash的堆
(4)當(dāng)你最終還是不知道是由哪邊的.so庫(kù)引起的話愿题,你可以查看下Native Heap的內(nèi)存分配情況损俭,這時(shí)候你依舊需要借助DDMS,
需要先執(zhí)行以下命令:
adb shell setprop libc.debug.malloc 1
adb shell stop
adb shell start
然后你還需要改一下eclipse中的配置參數(shù)值【因?yàn)槿绻悴慌渲玫脑捙诵铮愕腄DMS打開(kāi)默認(rèn)是看不到Native Heap那個(gè)Tab項(xiàng)的】
在ddms.cfg文件(實(shí)在找不到的話杆兵,就用Everything搜索下吧)最后增加一行native=true并save。ddms.cfg位于c:\Users\xxx.android目錄下仔夺。
在Device中選擇好你要的應(yīng)用的包名項(xiàng)琐脏,然后按下Snapshot按鈕, 就可以觀察到Native Heap的使用情況了缸兔,然后反復(fù)執(zhí)行腳本日裙,再觀察觀察,你會(huì)找到你需要的東西的惰蜜。
1.單例造成的內(nèi)存泄漏
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
//this.context = context.getApplicationContext(); 解決方式
}
public static AppManager getInstance(Context context) {
if (instance != null) {
instance = new AppManager(context);
}
return instance;
}
}
2.Handler造成的內(nèi)存泄漏
當(dāng) Android 應(yīng)用程序啟動(dòng)時(shí)昂拂,framework 會(huì)為該應(yīng)用程序的主線程創(chuàng)建一個(gè) Looper 對(duì)象。Looper 對(duì)象包含一個(gè)簡(jiǎn)單的消息隊(duì)列 Message Queue抛猖,并且能夠循環(huán)的處理隊(duì)列中的消息格侯。這些消息包括大多數(shù)應(yīng)用程序 framework 事件鼻听,例如 Activity 生命周期方法調(diào)用、button 點(diǎn)擊等联四,這些消息都會(huì)被添加到消息隊(duì)列中并被逐個(gè)處理撑碴。主線程的 Looper 對(duì)象會(huì)伴隨該應(yīng)用程序的整個(gè)生命周期。
當(dāng)我們?cè)谥骶€程中實(shí)例化一個(gè) Handler 對(duì)象后碎连,會(huì)自動(dòng)與主線程 Looper 的消息隊(duì)列關(guān)聯(lián)起來(lái)灰羽。所有發(fā)送到消息隊(duì)列的消息 Message 都會(huì)擁有一個(gè)對(duì) Handler 的引用,而此時(shí)當(dāng)前 Activity 如果已經(jīng)結(jié)束/銷毀鱼辙,而 Handler 由于是非靜態(tài)內(nèi)部類就會(huì)持有外部類的對(duì)象廉嚼,抓住當(dāng)前 Activity 對(duì)象不放,此時(shí)就極有可能導(dǎo)致內(nèi)存泄漏倒戏。
public class SampleActivity extends AppCompatActivity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 1);
// Go back to the previous Activity.
finish();
}
}
靜態(tài)內(nèi)部類不會(huì)持有外部類的引用怠噪,其跟外部類的關(guān)系,可以看成平級(jí)杜跷。
解決辦法就是使用靜態(tài)內(nèi)部類加 WeakRefrence傍念,如下所示:
private static class MyHandler extends Handler {
private final WeakReference<Sample2Activity> mActivity;
public MyHandler(Sample2Activity activity) {
mActivity = new WeakReference<Sample2Activity>(activity);
}
@Override
public void handleMessage (Message msg) {
Sample2Activity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
或者也可以在Activity的onDestory()中 removeCallbackandMessag(null)
3.非靜態(tài)內(nèi)部類持有外部類的實(shí)例
public class Sample4Activity extends AppCompatActivity {
private static LeakSample mLeakSample = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mLeakSample == null){
mLeakSample = new LeakSample();
}
//...
}
class LeakSample {
//...
}
}
上述代碼在 Activity 內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動(dòng) Activity 時(shí)都會(huì)使用該單例的數(shù)據(jù)(避免了資源的重復(fù)創(chuàng)建),這種寫(xiě)法卻會(huì)造成內(nèi)存泄漏葛闷,同樣因?yàn)榉庆o態(tài)內(nèi)部類持有外部類對(duì)象的原因憋槐。正確的做法為: 將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來(lái)封裝成一個(gè)單例,如果需要使用Context淑趾,請(qǐng)使用ApplicationContext阳仔。
屬性動(dòng)畫(huà)導(dǎo)致內(nèi)存泄漏
屬性動(dòng)畫(huà)中有一類無(wú)線循環(huán)的動(dòng)畫(huà),如果在當(dāng)前 Activity 中播放此類動(dòng)畫(huà)扣泊,并且沒(méi)有在結(jié)束的時(shí)候(onDestory)去停止該動(dòng)畫(huà)近范,那么動(dòng)畫(huà)會(huì)一直播放下去,盡管在界面上無(wú)法看見(jiàn)動(dòng)畫(huà)的運(yùn)轉(zhuǎn)延蟹,但是在此時(shí) Activity 的 View 會(huì)被動(dòng)畫(huà)所持有评矩,而 View 又持有當(dāng)前 Activity,最終導(dǎo)致 Activity 無(wú)法被釋放阱飘。動(dòng)畫(huà)的特征代碼如下:
animator.setRepeatCount(ValueAnimator.INFINITE);
解決辦法自然很簡(jiǎn)單斥杜,在 OnDestory() 中去取消動(dòng)畫(huà)即可。
Dialog 導(dǎo)致的內(nèi)存泄漏
在當(dāng)前 Dialog 所依附的 Activity 銷毀之前,我們沒(méi)有去將當(dāng)前的 Dialgo 銷毀(dismiss) 話也是很容易導(dǎo)致內(nèi)存泄漏的沥匈。
匿名內(nèi)部類
android開(kāi)發(fā)經(jīng)常會(huì)繼承實(shí)現(xiàn)Activity/Fragment/View果录,此時(shí)如果你使用了匿名類,并被異步線程持有了咐熙,那要小心了,如果沒(méi)有任何措施這樣一定會(huì)導(dǎo)致泄露
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}
ref1和ref2的區(qū)別是辨萍,ref2使用了匿名內(nèi)部類棋恼。我們來(lái)看看運(yùn)行時(shí)這兩個(gè)引用的內(nèi)存:
使用 Memory Profiler 查看 Java 堆和內(nèi)存分配
深入理解 Android 之內(nèi)存泄漏
Android 內(nèi)存泄漏總結(jié)
Android 內(nèi)存泄漏分析心得