內(nèi)存泄露指的是該釋放的對(duì)象沒有釋放,一直被某個(gè)或某些實(shí)例特持有卻不再被使用導(dǎo)致GC不能回收。
首先,我們先看看Java是怎樣來分配內(nèi)存的:
Java內(nèi)存分配策略
靜態(tài)分配
靜態(tài)分配使用靜態(tài)存儲(chǔ)區(qū)(方法區(qū)),主要存放靜態(tài)數(shù)據(jù)、全局static數(shù)據(jù)和常量。這塊內(nèi)存在程序編譯時(shí)就已經(jīng)分配好,并且在程序整個(gè)運(yùn)行期間都存在。
棧式分配
棧式分配使用棧區(qū)。當(dāng)方法被執(zhí)行時(shí),方法體內(nèi)的局部變量(其中包括基礎(chǔ)數(shù)據(jù)類型、對(duì)象的引用)都在棧上創(chuàng)建,并在方法執(zhí)行結(jié)束時(shí)這些局部變量所持有的內(nèi)存將會(huì)自動(dòng)被釋放。因?yàn)闂?nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。
堆式分配
堆式分配使用堆區(qū)。堆區(qū)又稱動(dòng)態(tài)內(nèi)存分配,通常就是指在程序運(yùn)行時(shí)直接 new 出來的內(nèi)存,也就是對(duì)象的實(shí)例。這部分內(nèi)存在不使用時(shí)將會(huì)由 Java 垃圾回收器來負(fù)責(zé)回收。
所以,Java內(nèi)存泄漏的根本原因就是:
長生命周期的對(duì)象持有短生命周期對(duì)象的引用就很可能發(fā)生內(nèi)存泄漏,盡管短生命周期對(duì)象已經(jīng)不再需要,但是因?yàn)殚L生命周期持有它的引用而導(dǎo)致不能被回收,這就是Java中內(nèi)存泄漏的發(fā)生場(chǎng)景,而內(nèi)存泄漏的區(qū)域就是堆區(qū)。
下面介紹Java和Android中引起內(nèi)存泄露的常見情況
靜態(tài)集合類引起內(nèi)存泄漏
像HashMap、Vector等的使用最容易出現(xiàn)內(nèi)存泄露,如果是靜態(tài)變量,這些靜態(tài)變量的生命周期和應(yīng)用程序一致,他們所引用的所有的對(duì)象Object也不能被釋放,因?yàn)樗麄円矊⒁恢北籚ector等引用著。如下程序,如果我們僅僅釋放引用本身,那么 Vector 仍然引用該對(duì)象,所以這個(gè)對(duì)象對(duì) GC 來說是不可回收的。因此,如果對(duì)象加入到Vector 后,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對(duì)象設(shè)置為 null。
在Android中,集合類如果僅僅有添加元素的方法,而沒有相應(yīng)的刪除機(jī)制,導(dǎo)致內(nèi)存被占用。如果這個(gè)集合類是全局性的變量 (比如類中的靜態(tài)屬性,全局性的 map 等即有靜態(tài)引用或 final 一直指向它),那么沒有相應(yīng)的刪除機(jī)制,很可能導(dǎo)致集合所占用的內(nèi)存只增不減。
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
}
當(dāng)集合里面的對(duì)象屬性被修改后,再調(diào)用remove()方法時(shí)不起作用
如下程序:
public static void main(String[] args)
{
Set<Person> set = new HashSet<Person>();
Person p1 = new Person("唐僧","pwd1",25);
Person p2 = new Person("孫悟空","pwd2",26);
Person p3 = new Person("豬八戒","pwd3",27);
set.add(p1);
set.add(p2);
set.add(p3);
System.out.println("總共有:"+set.size()+" 個(gè)元素!"); //結(jié)果:總共有:3 個(gè)元素!
p3.setAge(2); //修改p3的年齡,此時(shí)p3元素對(duì)應(yīng)的hashcode值發(fā)生改變
set.remove(p3); //此時(shí)remove不掉,造成內(nèi)存泄漏
set.add(p3); //重新添加,居然添加成功
System.out.println("總共有:"+set.size()+" 個(gè)元素!"); //結(jié)果:總共有:4 個(gè)元素!
for (Person person : set)
{
System.out.println(person);
}
}
監(jiān)聽器沒有釋放造成泄露
調(diào)用一個(gè)控件的諸如addXXXListener()等方法來增加監(jiān)聽器,但往往在釋放對(duì)象的時(shí)候卻沒有記住去刪除這些監(jiān)聽器,從而增加了內(nèi)存泄漏的機(jī)會(huì)。
在Android中的情況如下
系統(tǒng)服務(wù)可以通過Context.getSystemService 獲取,它們負(fù)責(zé)執(zhí)行某些后臺(tái)任務(wù),或者為硬件訪問提供接口。如果Context 對(duì)象想要在服務(wù)內(nèi)部的事件發(fā)生時(shí)被通知,那就需要把自己注冊(cè)到服務(wù)的監(jiān)聽器中。然而,這會(huì)讓服務(wù)持有Activity 的引用,如果在Activity onDestory時(shí)沒有釋放掉引用就會(huì)內(nèi)存泄漏。
解決方案:
1、使用ApplicationContext代替ActivityContext:
把
mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);
改成
mSensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
2、在Activity執(zhí)行onDestory時(shí),調(diào)用反注冊(cè);
protected void onDetachedFromWindow() {
if (this.mActionShell != null) { this.mActionShell.setOnClickListener((OnAreaClickListener)null);
}
if (this.mButtonShell != null) {
this.mButtonShell.setOnClickListener((OnAreaClickListener)null);
}
if (this.mCountShell != this.mCountShell) { this.mCountShell.setOnClickListener((OnAreaClickListener)null);
}
super.onDetachedFromWindow();
}
各種連接
比如數(shù)據(jù)庫連接(dataSourse.getConnection()),網(wǎng)絡(luò)連接(socket)和io連接,除非其顯式的調(diào)用了其close()方法將其連接關(guān)閉,否則是不會(huì)自動(dòng)被GC 回收的。對(duì)于Resultset 和Statement 對(duì)象可以不進(jìn)行顯式回收,但Connection 一定要顯式回收,因?yàn)镃onnection 在任何時(shí)候都無法自動(dòng)回收,而Connection一旦回收,Resultset 和Statement 對(duì)象就會(huì)立即為NULL。但是如果使用連接池,情況就不一樣了,除了要顯式地關(guān)閉連接,還必須顯式地關(guān)閉Resultset Statement 對(duì)象(關(guān)閉其中一個(gè),另外一個(gè)也會(huì)關(guān)閉),否則就會(huì)造成大量的Statement 對(duì)象無法釋放,從而引起內(nèi)存泄漏。這種情況下一般都會(huì)在try里面去的連接,在finally里面釋放連接。
內(nèi)部類和外部模塊的引用
內(nèi)部類的引用是比較容易遺忘的一種,而且一旦沒釋放可能導(dǎo)致一系列的后繼類對(duì)象沒有釋放。此外程序員還要小心外部模塊不經(jīng)意的引用,例如程序員A 負(fù)責(zé)A 模塊,調(diào)用了B 模塊的一個(gè)方法如: public void registerMsg(Object b); 這種調(diào)用就要非常小心了,傳入了一個(gè)對(duì)象,很可能模塊B就保持了對(duì)該對(duì)象的引用,這時(shí)候就需要注意模塊B 是否提供相應(yīng)的操作去除引用。
在Android中,非靜態(tài)內(nèi)部類也很容易引起的內(nèi)存泄露:
有的時(shí)候我們可能會(huì)在啟動(dòng)頻繁的Activity中,為了避免重復(fù)創(chuàng)建相同的數(shù)據(jù)資源,可能會(huì)出現(xiàn)這種寫法:
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mManager == null){
mManager = new TestResource();
}
//...
}
class TestResource {
//...
}
}
這樣就在Activity內(nèi)部創(chuàng)建了一個(gè)非靜態(tài)內(nèi)部類的單例,每次啟動(dòng)Activity時(shí)都會(huì)使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復(fù)創(chuàng)建,不過這種寫法卻會(huì)造成內(nèi)存泄漏,因?yàn)榉庆o態(tài)內(nèi)部類默認(rèn)會(huì)持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個(gè)靜態(tài)的實(shí)例,該實(shí)例的生命周期和應(yīng)用的一樣長,這就導(dǎo)致了該靜態(tài)實(shí)例一直會(huì)持有該Activity的引用,導(dǎo)致Activity的內(nèi)存資源不能正常回收。
正確的做法:
將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個(gè)單例,如果需要使用Context,請(qǐng)按照上面推薦的使用Application 的 Context(當(dāng)然,Application 的 context 不是萬能的,所以也不能隨便亂用,對(duì)于有些地方則必須使用 Activity 的 Context)。
android開發(fā)經(jīng)常會(huì)繼承實(shí)現(xiàn)Activity/Fragment/View,此時(shí)如果你使用了匿名類,并被異步線程持有了,那要小心了,如果沒有任何措施這樣一定會(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)部類。我們來看看運(yùn)行時(shí)這兩個(gè)引用的內(nèi)存:

可以看到,ref1沒什么特別的。但ref2這個(gè)匿名類的實(shí)現(xiàn)對(duì)象里面多了一個(gè)引用:this$0這個(gè)引用指向MainActivity.this,也就是說當(dāng)前的MainActivity實(shí)例會(huì)被ref2持有,如果將這個(gè)引用再傳入一個(gè)異步線程,此線程和此Acitivity生命周期不一致的時(shí)候,就造成了Activity的泄露。
單例模式引起的內(nèi)存泄露
不正確使用單例模式是引起內(nèi)存泄漏的一個(gè)常見問題,單例對(duì)象在初始化后將在JVM的整個(gè)生命周期中存在(以靜態(tài)變量的方式),如果單例對(duì)象持有外部的引用,那么這個(gè)對(duì)象將不能被JVM正?;厥?,導(dǎo)致內(nèi)存泄漏。
如下程序:顯然B采用singleton模式,它持有一個(gè)A對(duì)象的引用,而這個(gè)A類的對(duì)象將不能被回收。
class A{
public A(){
B.getInstance().setA(this);
}
....
}
//B類采用單例模式
class B{
private A a;
private static B instance=new B();
public B(){}
public static B getInstance(){
return instance;
}
public void setA(A a){
this.a=a;
}
//getter...
}
在Android中,單例類必須特別注意context的使用。由于單例的靜態(tài)特性使得其生命周期跟應(yīng)用的生命周期一樣長,所以如果使用不恰當(dāng)?shù)脑挘苋菀自斐蓛?nèi)存泄漏.
如下程序:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context;
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
這是一個(gè)普通的單例模式,當(dāng)創(chuàng)建這個(gè)單例的時(shí)候,由于需要傳入一個(gè)Context,所以這個(gè)Context的生命周期的長短至關(guān)重要:
1、如果此時(shí)傳入的是 Application 的 Context,因?yàn)?Application 的生命周期就是整個(gè)應(yīng)用的生命周期,所以這將沒有任何問題。
2、如果此時(shí)傳入的是 Activity 的 Context,當(dāng)這個(gè) Context 所對(duì)應(yīng)的 Activity 退出時(shí),由于該 Context 的引用被單例對(duì)象所持有,其生命周期等于整個(gè)應(yīng)用程序的生命周期,所以當(dāng)前 Activity 退出時(shí)它的內(nèi)存并不會(huì)被回收,這就造成泄漏了。
資源性對(duì)象Cursor,Stream沒有close,View沒有recyle
資源性對(duì)象比如(Cursor,F(xiàn)ile文件等)往往都用了一些緩沖,我們?cè)诓皇褂玫臅r(shí)候,應(yīng)該及時(shí)關(guān)閉它們,以便它們的緩沖及時(shí)回收內(nèi)存。它們的緩沖不僅存在于 java虛擬機(jī)內(nèi),還存在于java虛擬機(jī)外。如果我們僅僅是把它的引用設(shè)置為null,而不關(guān)閉它們,往往會(huì)造成內(nèi)存泄漏。因?yàn)橛行┵Y源性對(duì)象,比如SQLiteCursor(在析構(gòu)函數(shù)finalize(),如果我們沒有關(guān)閉它,它自己會(huì)調(diào)close()關(guān)閉),如果我們沒有關(guān)閉它,系統(tǒng)在回收它時(shí)也會(huì)關(guān)閉它,但是這樣的效率太低了。因此對(duì)于資源性對(duì)象在不使用的時(shí)候,應(yīng)該調(diào)用它的close()函數(shù),將其關(guān)閉掉,然后才置為null. 在我們的程序退出時(shí)一定要確保我們的資源性對(duì)象已經(jīng)關(guān)閉。
構(gòu)造Adapter時(shí),沒有使用緩存的ConvertView
初始時(shí)ListView會(huì)從Adapter中根據(jù)當(dāng)前的屏幕布局實(shí)例化一定數(shù)量的View對(duì)象,同時(shí)ListView會(huì)將這些View對(duì)象 緩存起來。
當(dāng)向上滾動(dòng)ListView時(shí),原先位于最上面的List Item的View對(duì)象會(huì)被回收,然后被用來構(gòu)造新出現(xiàn)的最下面的List Item。
這個(gè)構(gòu)造過程就是由getView()方法完成的,getView()的第二個(gè)形參View ConvertView就是被緩存起來的List Item的View對(duì)象(初始化時(shí)緩存中沒有View對(duì)象則ConvertView是null)。
Handler 造成的內(nèi)存泄漏
為了避免 ANR 而不在主線程進(jìn)行耗時(shí)操作,在處理網(wǎng)絡(luò)任務(wù)或者封裝一些請(qǐng)求回調(diào)等api都借助Handler來處理,但 Handler 不是萬能的,對(duì)于 Handler 的使用代碼編寫一不規(guī)范即有可能造成內(nèi)存泄漏。另外,我們知道 Handler、Message 和 MessageQueue 都是相互關(guān)聯(lián)在一起的,萬一 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對(duì)象將被線程 MessageQueue 一直持有。由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的。因此這種實(shí)現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致,故很容易導(dǎo)致無法正確釋放。
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { /* ... */ }
}, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
在該 SampleActivity 中聲明了一個(gè)延遲10分鐘執(zhí)行的消息 Message,mLeakyHandler 將其 push 進(jìn)了消息隊(duì)列 MessageQueue 里。當(dāng)該 Activity 被 finish() 掉時(shí),延遲執(zhí)行任務(wù)的 Message 還會(huì)繼續(xù)存在于主線程中,它持有該 Activity 的 Handler 引用,所以此時(shí) finish() 掉的 Activity 就不會(huì)被回收了從而造成內(nèi)存泄漏(因 Handler 為非靜態(tài)內(nèi)部類,它會(huì)持有外部類的引用,在這里就是指 SampleActivity)。
修復(fù)方法:在 Activity 中避免使用非靜態(tài)內(nèi)部類,比如上面我們將 Handler 聲明為靜態(tài)的,則其存活期跟 Activity 的生命周期就無關(guān)了。同時(shí)通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進(jìn)去:
public class SampleActivity extends Activity {
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
/**
* Instances of anonymous classes do not hold an implicit
* reference to their outer class when they are "static".
*/
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for 10 minutes.
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
// Go back to the previous Activity.
finish();
}
}
綜述,即推薦使用靜態(tài)內(nèi)部類 + WeakReference 這種方式。每次使用前注意判空。
創(chuàng)建一個(gè)靜態(tài)Handler內(nèi)部類,然后對(duì) Handler 持有的對(duì)象使用弱引用,這樣在回收時(shí)也可以回收 Handler 持有的對(duì)象,但是這樣做雖然避免了 Activity 泄漏,不過 Looper 線程的消息隊(duì)列中還是可能會(huì)有待處理的消息,所以我們?cè)?Activity 的 Destroy 時(shí)或者 Stop 時(shí)應(yīng)該移除消息隊(duì)列 MessageQueue 中的消息。
下面幾個(gè)方法都可以移除 Message:
public final void removeCallbacks(Runnable r);
public final void removeCallbacks(Runnable r, Object token);
public final void removeCallbacksAndMessages(Object token);
public final void removeMessages(int what);
public final void removeMessages(int what, Object object);
避免內(nèi)存泄露的方法
使用軟/弱引用
在Android應(yīng)用的開發(fā)中,為了防止內(nèi)存溢出,在處理一些占用內(nèi)存大而且聲明周期較長的對(duì)象時(shí)候,可以盡量應(yīng)用軟引用和弱引用技術(shù)。
盡量避免使用 static 成員變量
如果成員變量被聲明為 static,那我們都知道其生命周期將與整個(gè)app進(jìn)程生命周期一樣。
這會(huì)導(dǎo)致一系列問題,如果你的app進(jìn)程設(shè)計(jì)上是長駐內(nèi)存的,那即使app切到后臺(tái),這部分內(nèi)存也不會(huì)被釋放。按照現(xiàn)在手機(jī)app內(nèi)存管理機(jī)制,占內(nèi)存較大的后臺(tái)進(jìn)程將優(yōu)先回收,如果此app做過進(jìn)程互保?;睿菚?huì)造成app在后臺(tái)頻繁重啟。當(dāng)手機(jī)安裝了你參與開發(fā)的app以后一夜時(shí)間手機(jī)被消耗空了電量、流量,你的app不得不被用戶卸載或者靜默。
這里修復(fù)的方法是:
不要在類初始時(shí)初始化靜態(tài)成員??梢钥紤]lazy初始化。 架構(gòu)設(shè)計(jì)上要思考是否真的有必要這樣做,盡量避免。如果架構(gòu)需要這么設(shè)計(jì),那么此對(duì)象的生命周期你有責(zé)任管理起來。
避免重寫 finalize()
1、finalize 方法被執(zhí)行的時(shí)間不確定,不能依賴與它來釋放緊缺的資源。時(shí)間不確定的原因是: 虛擬機(jī)調(diào)用GC的時(shí)間不確定 Finalize daemon線程被調(diào)度到的時(shí)間不確定
2、finalize 方法只會(huì)被執(zhí)行一次,即使對(duì)象被復(fù)活,如果已經(jīng)執(zhí)行過了 finalize 方法,再次被 GC 時(shí)也不會(huì)再執(zhí)行了,原因是:含有 finalize 方法的 object 是在 new 的時(shí)候由虛擬機(jī)生成了一個(gè) finalize reference 在來引用到該Object的,而在 finalize 方法執(zhí)行的時(shí)候,該 object 所對(duì)應(yīng)的 finalize Reference 會(huì)被釋放掉,即使在這個(gè)時(shí)候把該 object 復(fù)活(即用強(qiáng)引用引用住該 object ),再第二次被 GC 的時(shí)候由于沒有了 finalize reference 與之對(duì)應(yīng),所以 finalize 方法不會(huì)再執(zhí)行。
3、含有Finalize方法的object需要至少經(jīng)過兩輪GC才有可能被釋放。