內(nèi)存泄露指的是該釋放的對象沒有釋放角钩,一直被某個或某些實例特持有卻不再被使用導致GC不能回收骚灸。
首先司抱,我們先看看Java是怎樣來分配內(nèi)存的:
Java內(nèi)存分配策略
靜態(tài)分配
靜態(tài)分配使用靜態(tài)存儲區(qū)(方法區(qū)),主要存放靜態(tài)數(shù)據(jù)驻呐、全局static數(shù)據(jù)和常量毁渗。這塊內(nèi)存在程序編譯時就已經(jīng)分配好践磅,并且在程序整個運行期間都存在。
棧式分配
棧式分配使用棧區(qū)灸异。當方法被執(zhí)行時府适,方法體內(nèi)的局部變量(其中包括基礎數(shù)據(jù)類型羔飞、對象的引用)都在棧上創(chuàng)建,并在方法執(zhí)行結(jié)束時這些局部變量所持有的內(nèi)存將會自動被釋放檐春。因為棧內(nèi)存分配運算內(nèi)置于處理器的指令集中逻淌,效率很高,但是分配的內(nèi)存容量有限喇聊。
堆式分配
堆式分配使用堆區(qū)恍风。堆區(qū)又稱動態(tài)內(nèi)存分配,通常就是指在程序運行時直接 new 出來的內(nèi)存誓篱,也就是對象的實例。這部分內(nèi)存在不使用時將會由 Java 垃圾回收器來負責回收凯楔。
所以窜骄,Java內(nèi)存泄漏的根本原因就是:
長生命周期的對象持有短生命周期對象的引用就很可能發(fā)生內(nèi)存泄漏,盡管短生命周期對象已經(jīng)不再需要摆屯,但是因為長生命周期持有它的引用而導致不能被回收邻遏,這就是Java中內(nèi)存泄漏的發(fā)生場景,而內(nèi)存泄漏的區(qū)域就是堆區(qū)虐骑。
下面介紹Java和Android中引起內(nèi)存泄露的常見情況
靜態(tài)集合類引起內(nèi)存泄漏
像HashMap准验、Vector等的使用最容易出現(xiàn)內(nèi)存泄露,如果是靜態(tài)變量廷没,這些靜態(tài)變量的生命周期和應用程序一致糊饱,他們所引用的所有的對象Object也不能被釋放,因為他們也將一直被Vector等引用著颠黎。如下程序另锋,如果我們僅僅釋放引用本身,那么 Vector 仍然引用該對象狭归,所以這個對象對 GC 來說是不可回收的夭坪。因此,如果對象加入到Vector 后过椎,還必須從 Vector 中刪除室梅,最簡單的方法就是將 Vector 對象設置為 null。
在Android中疚宇,集合類如果僅僅有添加元素的方法亡鼠,而沒有相應的刪除機制,導致內(nèi)存被占用灰嫉。如果這個集合類是全局性的變量 (比如類中的靜態(tài)屬性拆宛,全局性的 map 等即有靜態(tài)引用或 final 一直指向它),那么沒有相應的刪除機制讼撒,很可能導致集合所占用的內(nèi)存只增不減浑厚。
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
}
當集合里面的對象屬性被修改后股耽,再調(diào)用remove()方法時不起作用
如下程序:
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()+" 個元素!"); //結(jié)果:總共有:3 個元素!
p3.setAge(2); //修改p3的年齡,此時p3元素對應的hashcode值發(fā)生改變
set.remove(p3); //此時remove不掉,造成內(nèi)存泄漏
set.add(p3); //重新添加钳幅,居然添加成功
System.out.println("總共有:"+set.size()+" 個元素!"); //結(jié)果:總共有:4 個元素!
for (Person person : set)
{
System.out.println(person);
}
}
監(jiān)聽器沒有釋放造成泄露
調(diào)用一個控件的諸如addXXXListener()等方法來增加監(jiān)聽器物蝙,但往往在釋放對象的時候卻沒有記住去刪除這些監(jiān)聽器,從而增加了內(nèi)存泄漏的機會敢艰。
在Android中的情況如下
系統(tǒng)服務可以通過Context.getSystemService 獲取诬乞,它們負責執(zhí)行某些后臺任務,或者為硬件訪問提供接口钠导。如果Context 對象想要在服務內(nèi)部的事件發(fā)生時被通知震嫉,那就需要把自己注冊到服務的監(jiān)聽器中。然而牡属,這會讓服務持有Activity 的引用票堵,如果在Activity onDestory時沒有釋放掉引用就會內(nèi)存泄漏。
解決方案:
1逮栅、使用ApplicationContext代替ActivityContext:
把
mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);
改成
mSensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
2悴势、在Activity執(zhí)行onDestory時,調(diào)用反注冊;
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)絡連接(socket)和io連接特纤,除非其顯式的調(diào)用了其close()方法將其連接關閉,否則是不會自動被GC 回收的侥加。對于Resultset 和Statement 對象可以不進行顯式回收捧存,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收官硝,而Connection一旦回收矗蕊,Resultset 和Statement 對象就會立即為NULL。但是如果使用連接池氢架,情況就不一樣了傻咖,除了要顯式地關閉連接,還必須顯式地關閉Resultset Statement 對象(關閉其中一個岖研,另外一個也會關閉)卿操,否則就會造成大量的Statement 對象無法釋放,從而引起內(nèi)存泄漏孙援。這種情況下一般都會在try里面去的連接害淤,在finally里面釋放連接。
內(nèi)部類和外部模塊的引用
內(nèi)部類的引用是比較容易遺忘的一種拓售,而且一旦沒釋放可能導致一系列的后繼類對象沒有釋放窥摄。此外程序員還要小心外部模塊不經(jīng)意的引用,例如程序員A 負責A 模塊础淤,調(diào)用了B 模塊的一個方法如: public void registerMsg(Object b); 這種調(diào)用就要非常小心了崭放,傳入了一個對象哨苛,很可能模塊B就保持了對該對象的引用,這時候就需要注意模塊B 是否提供相應的操作去除引用币砂。
在Android中建峭,非靜態(tài)內(nèi)部類也很容易引起的內(nèi)存泄露:
有的時候我們可能會在啟動頻繁的Activity中,為了避免重復創(chuàng)建相同的數(shù)據(jù)資源决摧,可能會出現(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)建了一個非靜態(tài)內(nèi)部類的單例亿蒸,每次啟動Activity時都會使用該單例的數(shù)據(jù),這樣雖然避免了資源的重復創(chuàng)建掌桩,不過這種寫法卻會造成內(nèi)存泄漏边锁,因為非靜態(tài)內(nèi)部類默認會持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個靜態(tài)的實例波岛,該實例的生命周期和應用的一樣長砚蓬,這就導致了該靜態(tài)實例一直會持有該Activity的引用,導致Activity的內(nèi)存資源不能正撑枭回收。
正確的做法:
將該內(nèi)部類設為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個單例祟剔,如果需要使用Context隔躲,請按照上面推薦的使用Application 的 Context(當然,Application 的 context 不是萬能的物延,所以也不能隨便亂用宣旱,對于有些地方則必須使用 Activity 的 Context)。
android開發(fā)經(jīng)常會繼承實現(xiàn)Activity/Fragment/View叛薯,此時如果你使用了匿名類浑吟,并被異步線程持有了,那要小心了耗溜,如果沒有任何措施這樣一定會導致泄露:
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}
ref1和ref2的區(qū)別是组力,ref2使用了匿名內(nèi)部類。我們來看看運行時這兩個引用的內(nèi)存:
可以看到抖拴,ref1沒什么特別的燎字。但ref2這個匿名類的實現(xiàn)對象里面多了一個引用:this$0這個引用指向MainActivity.this,也就是說當前的MainActivity實例會被ref2持有阿宅,如果將這個引用再傳入一個異步線程候衍,此線程和此Acitivity生命周期不一致的時候,就造成了Activity的泄露洒放。
單例模式引起的內(nèi)存泄露
不正確使用單例模式是引起內(nèi)存泄漏的一個常見問題蛉鹿,單例對象在初始化后將在JVM的整個生命周期中存在(以靜態(tài)變量的方式),如果單例對象持有外部的引用往湿,那么這個對象將不能被JVM正逞欤回收惋戏,導致內(nèi)存泄漏。
如下程序:顯然B采用singleton模式随闺,它持有一個A對象的引用日川,而這個A類的對象將不能被回收。
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)特性使得其生命周期跟應用的生命周期一樣長,所以如果使用不恰當?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;
}
}
這是一個普通的單例模式分歇,當創(chuàng)建這個單例的時候,由于需要傳入一個Context欧漱,所以這個Context的生命周期的長短至關重要:
1职抡、如果此時傳入的是 Application 的 Context,因為 Application 的生命周期就是整個應用的生命周期误甚,所以這將沒有任何問題缚甩。
2、如果此時傳入的是 Activity 的 Context窑邦,當這個 Context 所對應的 Activity 退出時擅威,由于該 Context 的引用被單例對象所持有,其生命周期等于整個應用程序的生命周期冈钦,所以當前 Activity 退出時它的內(nèi)存并不會被回收郊丛,這就造成泄漏了。
資源性對象Cursor瞧筛,Stream沒有close厉熟,View沒有recyle
資源性對象比如(Cursor,F(xiàn)ile文件等)往往都用了一些緩沖较幌,我們在不使用的時候揍瑟,應該及時關閉它們,以便它們的緩沖及時回收內(nèi)存绅络。它們的緩沖不僅存在于 java虛擬機內(nèi)月培,還存在于java虛擬機外。如果我們僅僅是把它的引用設置為null,而不關閉它們恩急,往往會造成內(nèi)存泄漏杉畜。因為有些資源性對象,比如SQLiteCursor(在析構(gòu)函數(shù)finalize(),如果我們沒有關閉它衷恭,它自己會調(diào)close()關閉)此叠,如果我們沒有關閉它,系統(tǒng)在回收它時也會關閉它随珠,但是這樣的效率太低了灭袁。因此對于資源性對象在不使用的時候猬错,應該調(diào)用它的close()函數(shù),將其關閉掉茸歧,然后才置為null. 在我們的程序退出時一定要確保我們的資源性對象已經(jīng)關閉倦炒。
構(gòu)造Adapter時,沒有使用緩存的ConvertView
初始時ListView會從Adapter中根據(jù)當前的屏幕布局實例化一定數(shù)量的View對象软瞎,同時ListView會將這些View對象 緩存起來逢唤。
當向上滾動ListView時,原先位于最上面的List Item的View對象會被回收涤浇,然后被用來構(gòu)造新出現(xiàn)的最下面的List Item鳖藕。
這個構(gòu)造過程就是由getView()方法完成的,getView()的第二個形參View ConvertView就是被緩存起來的List Item的View對象(初始化時緩存中沒有View對象則ConvertView是null)只锭。
Handler 造成的內(nèi)存泄漏
為了避免 ANR 而不在主線程進行耗時操作著恩,在處理網(wǎng)絡任務或者封裝一些請求回調(diào)等api都借助Handler來處理,但 Handler 不是萬能的蜻展,對于 Handler 的使用代碼編寫一不規(guī)范即有可能造成內(nèi)存泄漏喉誊。另外,我們知道 Handler纵顾、Message 和 MessageQueue 都是相互關聯(lián)在一起的裹驰,萬一 Handler 發(fā)送的 Message 尚未被處理,則該 Message 及發(fā)送它的 Handler 對象將被線程 MessageQueue 一直持有片挂。由于 Handler 屬于 TLS(Thread Local Storage) 變量, 生命周期和 Activity 是不一致的。因此這種實現(xiàn)方式一般很難保證跟 View 或者 Activity 的生命周期保持一致贞盯,故很容易導致無法正確釋放音念。
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 中聲明了一個延遲10分鐘執(zhí)行的消息 Message,mLeakyHandler 將其 push 進了消息隊列 MessageQueue 里躏敢。當該 Activity 被 finish() 掉時闷愤,延遲執(zhí)行任務的 Message 還會繼續(xù)存在于主線程中,它持有該 Activity 的 Handler 引用件余,所以此時 finish() 掉的 Activity 就不會被回收了從而造成內(nèi)存泄漏(因 Handler 為非靜態(tài)內(nèi)部類讥脐,它會持有外部類的引用,在這里就是指 SampleActivity)啼器。
修復方法:在 Activity 中避免使用非靜態(tài)內(nèi)部類旬渠,比如上面我們將 Handler 聲明為靜態(tài)的,則其存活期跟 Activity 的生命周期就無關了端壳。同時通過弱引用的方式引入 Activity告丢,避免直接將 Activity 作為 context 傳進去:
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)建一個靜態(tài)Handler內(nèi)部類岳颇,然后對 Handler 持有的對象使用弱引用,這樣在回收時也可以回收 Handler 持有的對象颅湘,但是這樣做雖然避免了 Activity 泄漏话侧,不過 Looper 線程的消息隊列中還是可能會有待處理的消息,所以我們在 Activity 的 Destroy 時或者 Stop 時應該移除消息隊列 MessageQueue 中的消息闯参。
下面幾個方法都可以移除 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應用的開發(fā)中瞻鹏,為了防止內(nèi)存溢出,在處理一些占用內(nèi)存大而且聲明周期較長的對象時候赢赊,可以盡量應用軟引用和弱引用技術乙漓。
盡量避免使用 static 成員變量
如果成員變量被聲明為 static,那我們都知道其生命周期將與整個app進程生命周期一樣释移。
這會導致一系列問題叭披,如果你的app進程設計上是長駐內(nèi)存的,那即使app切到后臺玩讳,這部分內(nèi)存也不會被釋放涩蜘。按照現(xiàn)在手機app內(nèi)存管理機制,占內(nèi)存較大的后臺進程將優(yōu)先回收熏纯,如果此app做過進程互保蓖耄活,那會造成app在后臺頻繁重啟樟澜。當手機安裝了你參與開發(fā)的app以后一夜時間手機被消耗空了電量误窖、流量,你的app不得不被用戶卸載或者靜默秩贰。
這里修復的方法是:
不要在類初始時初始化靜態(tài)成員霹俺。可以考慮lazy初始化毒费。 架構(gòu)設計上要思考是否真的有必要這樣做丙唧,盡量避免。如果架構(gòu)需要這么設計觅玻,那么此對象的生命周期你有責任管理起來想际。
避免重寫 finalize()
1、finalize 方法被執(zhí)行的時間不確定溪厘,不能依賴與它來釋放緊缺的資源胡本。時間不確定的原因是: 虛擬機調(diào)用GC的時間不確定 Finalize daemon線程被調(diào)度到的時間不確定
2、finalize 方法只會被執(zhí)行一次畸悬,即使對象被復活打瘪,如果已經(jīng)執(zhí)行過了 finalize 方法,再次被 GC 時也不會再執(zhí)行了,原因是:含有 finalize 方法的 object 是在 new 的時候由虛擬機生成了一個 finalize reference 在來引用到該Object的闺骚,而在 finalize 方法執(zhí)行的時候彩扔,該 object 所對應的 finalize Reference 會被釋放掉,即使在這個時候把該 object 復活(即用強引用引用住該 object )僻爽,再第二次被 GC 的時候由于沒有了 finalize reference 與之對應虫碉,所以 finalize 方法不會再執(zhí)行。
3胸梆、含有Finalize方法的object需要至少經(jīng)過兩輪GC才有可能被釋放敦捧。