作者:developerHaoz
地址:https://www.jianshu.com/p/65f914e6a2f8
聲明:本文是 developerHaoz 原創(chuàng)投稿,轉(zhuǎn)發(fā)等請(qǐng)聯(lián)系原作者授權(quán)。
內(nèi)存管理的目的就是讓我們?cè)陂_(kāi)發(fā)過(guò)程中有效避免我們的應(yīng)用程序出現(xiàn)內(nèi)存泄露的問(wèn)題。內(nèi)存泄露相信大家都不陌生,我們可以這樣理解:「沒(méi)有用的對(duì)象無(wú)法回收的現(xiàn)象就是內(nèi)存泄露」。
如果程序發(fā)生了內(nèi)存泄露,則會(huì)帶來(lái)以下這些問(wèn)題
應(yīng)用可用的內(nèi)存減少,增加了堆內(nèi)存的壓力
降低了應(yīng)用的性能,比如會(huì)觸發(fā)更頻繁的 GC
嚴(yán)重的時(shí)候可能會(huì)導(dǎo)致內(nèi)存溢出錯(cuò)誤,即 OOM Error
OOM 發(fā)生在,當(dāng)我們嘗試進(jìn)行創(chuàng)建對(duì)象,但是堆內(nèi)存無(wú)法通過(guò) GC 釋放足夠的空間,堆內(nèi)存也無(wú)法再繼續(xù)增長(zhǎng),從而完成對(duì)象創(chuàng)建請(qǐng)求的時(shí)候,OOM 發(fā)生很有可能是內(nèi)存泄露導(dǎo)致的,但并非所有的 OOM 都是由內(nèi)存泄露引起的,內(nèi)存泄露也并不一定引起 OOM。
如果真的想比較清楚的了解內(nèi)存泄露的話,對(duì)于 Java 的內(nèi)存管理以及引用類型有一個(gè)清晰的認(rèn)識(shí)是必不可少的。
理解 Java 的內(nèi)存管理能讓我們更深一層地了解 Java 虛擬機(jī)是怎樣使用內(nèi)存的,一旦出現(xiàn)內(nèi)存泄露,我們也能更加從容地排查問(wèn)題。
了解 Java 的引用類型,能讓我們更加理解內(nèi)存泄露出現(xiàn)的原因,以及常見(jiàn)的解決方法。
具體的內(nèi)容,可以看下這篇文章 你真的懂 Java 的內(nèi)存管理和引用類型嗎?
單例模式是非常常用的設(shè)計(jì)模式,使用單例模式的類,只會(huì)產(chǎn)生一個(gè)對(duì)象,這個(gè)對(duì)象看起來(lái)像是一直占用著內(nèi)存,但這并不意味著就是浪費(fèi)了內(nèi)存,內(nèi)存本來(lái)就是拿來(lái)裝東西的,只要這個(gè)對(duì)象一直都被高效的利用就不能叫做泄露。
但是過(guò)多的單例會(huì)讓內(nèi)存占用過(guò)多,而且單例模式由于其 靜態(tài)特性,其生命周期 = 應(yīng)用程序的生命周期,不正確地使用單例模式也會(huì)造成內(nèi)存泄露。
舉個(gè)例子:
public class SingleInstanceTest {
private static SingleInstanceTest sInstance;
private Context mContext;
private SingleInstanceTest(Context context){
this.mContext = context;
}
public static SingleInstanceTest newInstance(Context context){
if(sInstance == null){
sInstance = new SingleInstanceTest(context);
}
return sInstance;
}
}
上面是一個(gè)比較簡(jiǎn)單的單例模式用法,需要外部傳入一個(gè) Context 來(lái)獲取該類的實(shí)例,如果此時(shí)傳入的 Context 是 Activity 的話,此時(shí)單例就有持有該 Activity 的強(qiáng)引用(直到整個(gè)應(yīng)用生命周期結(jié)束)。這樣的話,即使該 Activity 退出,該 Activity 的內(nèi)存也不會(huì)被回收,這樣就造成了內(nèi)存泄露,特別是一些比較大的 Activity,甚至還會(huì)導(dǎo)致 OOM(Out Of Memory)。
解決方法:單例模式引用的對(duì)象的生命周期 = 應(yīng)用生命周期
public class SingleInstanceTest {
private static SingleInstanceTest sInstance;
private Context mContext;
private SingleInstanceTest(Context context){
this.mContext = context.getApplicationContext();
}
public static SingleInstanceTest newInstance(Context context){
if(sInstance == null){
sInstance = new SingleInstanceTest(context);
}
return sInstance;
}
}
可以看到在 SingleInstanceTest 的構(gòu)造函數(shù)中,將 context.getApplicationContext() 賦值給 mContext,此時(shí)單例引用的對(duì)象是 Application,而 Application 的生命周期本來(lái)就跟應(yīng)用程序是一樣的,也就不存在內(nèi)存泄露。
這里再拓展一點(diǎn),很多時(shí)候我們?cè)谛枰玫?Activity 或者 Context 的地方,會(huì)直接將 Activity 的實(shí)例作為參數(shù)傳給對(duì)應(yīng)的類,就像這樣:
public class Sample {
private Context mContext;
public Sample(Context context){
this.mContext = context;
}
public Context getContext() {
return mContext;
}
}
// 外部調(diào)用
Sample sample = new Sample(MainActivity.this);
這種情況如果不注意的話,很容易就會(huì)造成內(nèi)存泄露,比較好的寫(xiě)法是使用弱引用(WeakReference)來(lái)進(jìn)行改進(jìn)。
public class Sample {
private WeakReference<Context> mWeakReference;
public Sample(Context context){
this.mWeakReference = new WeakReference<>(context);
}
public Context getContext() {
if(mWeakReference.get() != null){
return mWeakReference.get();
}
return null;
}
}
// 外部調(diào)用
Sample sample = new Sample(MainActivity.this);
被弱引用關(guān)聯(lián)的對(duì)象只能存活到下一次垃圾回收之前,也就是說(shuō)即使 Sample 持有 Activity 的引用,但由于 GC 會(huì)幫我們回收相關(guān)的引用,被銷毀的 Activity 也會(huì)被回收內(nèi)存,這樣我們就不用擔(dān)心會(huì)發(fā)生內(nèi)存泄露了。
我們先來(lái)看看非靜態(tài)內(nèi)部類(non static inner class)和 靜態(tài)內(nèi)部類(static inner class)之間的區(qū)別。
class 對(duì)比 | static inner class | non static inner class |
---|---|---|
與外部 class 引用關(guān)系 | 如果沒(méi)有傳入?yún)?shù),就沒(méi)有引用關(guān)系 | 自動(dòng)獲得強(qiáng)引用 |
被調(diào)用時(shí)需要外部實(shí)例 | 不需要 | 需要 |
能否調(diào)用外部 class 中的變量和方法 | 不能 | 能 |
生命周期 | 自主的生命周期 | 依賴于外部類,甚至比外部類更長(zhǎng) |
可以看到非靜態(tài)內(nèi)部類自動(dòng)獲得外部類的強(qiáng)引用,而且它的生命周期甚至比外部類更長(zhǎng),這便埋下了內(nèi)存泄露的隱患。如果一個(gè) Activity 的非靜態(tài)內(nèi)部類的生命周期比 Activity 更長(zhǎng),那么 Activity 的內(nèi)存便無(wú)法被回收,也就是發(fā)生了內(nèi)存泄露,而且還有可能發(fā)生難以預(yù)防的空指針問(wèn)題。
舉個(gè)例子:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MyAscnyTask().execute();
}
class MyAscnyTask extends AsyncTask<Void, Integer, String>{
@Override
protected String doInBackground(Void... params) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "";
}
}
}
可以看到我們?cè)?Activity 中繼承 AsyncTask 自定義了一個(gè)非靜態(tài)內(nèi)部類,在 doInbackground() 方法中做了耗時(shí)的操作,然后在 onCreate() 中啟動(dòng) MyAsyncTask。如果在耗時(shí)操作結(jié)束之前,Activity 被銷毀了,這時(shí)候因?yàn)?MyAsyncTask 持有 Activity 的強(qiáng)引用,便會(huì)導(dǎo)致 Activity 的內(nèi)存無(wú)法被回收,這時(shí)候便會(huì)產(chǎn)生內(nèi)存泄露。
解決方法:將 MyAsyncTask 變成非靜態(tài)內(nèi)部類
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new MyAscnyTask().execute();
}
static class MyAscnyTask extends AsyncTask<Void, Integer, String>{
@Override
protected String doInBackground(Void... params) {
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "";
}
}
}
這時(shí)候 MyAsyncTask 不再持有 Activity 的強(qiáng)引用,即使 AsyncTask 的耗時(shí)操作還在繼續(xù),Activity 的內(nèi)存也能順利地被回收。
匿名類和非靜態(tài)內(nèi)部類最大的共同點(diǎn)就是 都持有外部類的引用,因此,匿名類造成內(nèi)存泄露的原因也跟靜態(tài)內(nèi)部類基本是一樣的,下面舉個(gè)幾個(gè)比較常見(jiàn)的例子:
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// ① 匿名線程持有 Activity 的引用,進(jìn)行耗時(shí)操作
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// ② 使用匿名 Handler 發(fā)送耗時(shí)消息
Message message = Message.obtain();
mHandler.sendMessageDelayed(message, 60000);
}
上面舉出了兩個(gè)比較常見(jiàn)的例子
new 出一個(gè)匿名的 Thread,進(jìn)行耗時(shí)的操作,如果 MainActivity 被銷毀而 Thread 中的耗時(shí)操作沒(méi)有結(jié)束的話,便會(huì)產(chǎn)生內(nèi)存泄露
new 出一個(gè)匿名的 Handler,這里我采用了 sendMessageDelayed() 方法來(lái)發(fā)送消息,這時(shí)如果 MainActivity 被銷毀,而 Handler 里面的消息還沒(méi)發(fā)送完畢的話,Activity 的內(nèi)存也不會(huì)被回收
解決方法:
繼承 Thread 實(shí)現(xiàn)靜態(tài)內(nèi)部類
繼承 Handler 實(shí)現(xiàn)靜態(tài)內(nèi)部類,以及在 Activity 的 onDestroy() 方法中,移除所有的消息 mHandler.removeCallbacksAndMessages(null);
集合類添加元素后,仍引用著集合元素對(duì)象,導(dǎo)致該集合中的元素對(duì)象無(wú)法被回收,從而導(dǎo)致內(nèi)存泄露,舉個(gè)例子:
static List<Object> objectList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Object obj = new Object();
objectList.add(obj);
obj = null;
}
在這個(gè)例子中,循環(huán)多次將 new 出來(lái)的對(duì)象放入一個(gè)靜態(tài)的集合中,因?yàn)殪o態(tài)變量的生命周期和應(yīng)用程序一致,而且他們所引用的對(duì)象 Object 也不能釋放,這樣便造成了內(nèi)存泄露。
解決方法:在集合元素使用之后從集合中刪除,等所有元素都使用完之后,將集合置空。
objectList.clear();
objectList = null;
除了上述 3 種常見(jiàn)情況外,還有其他的一些情況
1、需要手動(dòng)關(guān)閉的對(duì)象沒(méi)有關(guān)閉
網(wǎng)絡(luò)、文件等流忘記關(guān)閉
手動(dòng)注冊(cè)廣播時(shí),退出時(shí)忘記 unregisterReceiver()
Service 執(zhí)行完后忘記 stopSelf()
EventBus 等觀察者模式的框架忘記手動(dòng)解除注冊(cè)
2、static 關(guān)鍵字修飾的成員變量
3、ListView 的 Item 泄露
除了必須了解常見(jiàn)的內(nèi)存泄露場(chǎng)景以及相應(yīng)的解決方法之外,掌握一些好用的工具,能讓我們更有效率地解決內(nèi)存泄露的問(wèn)題。
Lint 是 Android Studio 提供的 代碼掃描分析工具,它可以幫助我們發(fā)現(xiàn)代碼機(jī)構(gòu) / 質(zhì)量問(wèn)題,同時(shí)提供一些解決方案,檢測(cè)內(nèi)存泄露當(dāng)然也不在話下,使用也是非常的簡(jiǎn)單,可以參考下這篇文章:Android 性能優(yōu)化:使用 Lint 優(yōu)化代碼、去除多余資源
LeakCanary 是 Square 公司開(kāi)源的「Android 和 Java 的內(nèi)存泄漏檢測(cè)庫(kù)」,Square 出品,必屬精品,功能很強(qiáng)大,使用也很簡(jiǎn)單。建議直接看 Github 上的說(shuō)明:leakcanary,也可以參考這篇文章:Android內(nèi)存優(yōu)化(六)LeakCanary使用詳解
Android 內(nèi)存泄露分析
Android 性能優(yōu)化:手把手帶你全面了解內(nèi)存泄露
系統(tǒng)剖析Android中的內(nèi)存泄漏
聯(lián)系客服