一、內(nèi)存泄漏的根本原因
內(nèi)存泄漏的根本原因在于生命周期長的對象持有了生命周期短的對象的引用
二、內(nèi)存泄漏的場景
1、全局集合類強引用造成的內(nèi)存泄漏(特別是 static 修飾的集合)
2、接收器、監(jiān)聽器注冊沒取消造成的內(nèi)存泄漏,如廣播,eventsbus
3、各種連接沒有及時關(guān)閉造成的內(nèi)存泄漏
4、單例造成的內(nèi)存泄漏
5、匿名內(nèi)部類/非靜態(tài)內(nèi)部類和異步線程持有生命周期短的引用導(dǎo)致的內(nèi)存泄漏(比如非靜態(tài)handler內(nèi)部類持有外部類Activity的引用)
后面再分別講述原因和解決辦法。
說到內(nèi)存泄漏,就一定要講一講java的內(nèi)存分配機制和gc的工作原理
三、Java 內(nèi)存分配策略
Java 程序運行時的內(nèi)存分配策略有三種,分別是靜態(tài)分配,棧式分配,和堆式分配,對應(yīng)的,三種存儲策略使用的內(nèi)存空間主要分別是靜態(tài)存儲區(qū)(也稱方法區(qū))、棧區(qū)和堆區(qū)。
靜態(tài)存儲區(qū)(方法區(qū)):主要存放靜態(tài)數(shù)據(jù)、全局 static 數(shù)據(jù)和常量。這塊內(nèi)存在程序編譯時就已經(jīng)分配好,并且在程序整個運行期間都存在。
棧區(qū) :當方法被執(zhí)行時,方法體內(nèi)的局部變量(其中包括基礎(chǔ)數(shù)據(jù)類型、對象的引用)都在棧上創(chuàng)建,并在方法執(zhí)行結(jié)束時這些局部變量所持有的內(nèi)存將會自動被釋放。因為棧內(nèi)存分配運算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。
堆區(qū) : 又稱動態(tài)內(nèi)存分配,通常就是指在程序運行時直接 new 出來的內(nèi)存,也就是對象的實例。這部分內(nèi)存在不使用時將會由 Java 垃圾回收器來負責(zé)回收。
棧與堆的區(qū)別:
在方法體內(nèi)定義的(局部變量)一些基本類型的變量和對象的引用變量都是在方法的棧內(nèi)存中分配的。當在一段方法塊中定義一個變量時,Java 就會在棧中為該變量分配內(nèi)存空間,當超過該變量的作用域后,該變量也就無效了,分配給它的內(nèi)存空間也將被釋放掉,該內(nèi)存空間可以被重新使用。
堆內(nèi)存用來存放所有由 new 創(chuàng)建的對象(包括該對象其中的所有成員變量)和數(shù)組。在堆中分配的內(nèi)存,將由 Java 垃圾回收器來自動管理。在堆中產(chǎn)生了一個數(shù)組或者對象后,還可以在棧中定義一個特殊的變量,這個變量的取值等于數(shù)組或者對象在堆內(nèi)存中的首地址,這個特殊的變量就是我們上面說的引用變量。我們可以通過這個引用變量來訪問堆中的對象或者數(shù)組。
舉個例子:
public class Sample {
int s1 = 0;
Sample mSample1 = new Sample();
public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}
Sample mSample3 = new Sample();
Sample 類的局部變量 s2 和引用變量 mSample2 都是存在于棧中,但 mSample2 指向的對象是存在于堆上的。
mSample3 指向的對象實體存放在堆上,包括這個對象的所有成員變量 s1 和 mSample1,而它自己存在于棧中。
結(jié)論:
局部變量的基本數(shù)據(jù)類型和引用存儲于棧中,引用的對象實體存儲于堆中?!?因為它們屬于方法中的變量,生命周期隨方法而結(jié)束。
成員變量全部存儲與堆中(包括基本數(shù)據(jù)類型,引用和引用的對象實體)—— 因為它們屬于類,類對象終究是要被new出來使用的。
了解了 Java 的內(nèi)存分配之后,我們再來看看 Java 是怎么管理內(nèi)存的。
Java是如何管理內(nèi)存
Java的內(nèi)存管理就是對象的分配和釋放問題。在 Java 中,程序員需要通過關(guān)鍵字 new 為每個對象申請內(nèi)存空間 (基本類型除外),所有的對象都在堆 (Heap)中分配空間。另外,對象的釋放是由 GC 決定和執(zhí)行的。在 Java 中,內(nèi)存的分配是由程序完成的,而內(nèi)存的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程序員的工作。但同時,它也加重了JVM的工作。這也是 Java 程序運行速度較慢的原因之一。因為,GC 為了能夠正確釋放對象,GC 必須監(jiān)控每一個對象的運行狀態(tài),包括對象的申請、引用、被引用、賦值等,GC 都需要進行監(jiān)控。
監(jiān)視對象狀態(tài)是為了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。
為了更好理解 GC 的工作原理,我們可以將對象考慮為有向圖的頂點,將引用關(guān)系考慮為圖的有向邊,有向邊從引用者指向被引對象。另外,每個線程對象可以作為一個圖的起始頂點,例如大多程序從 main 進程開始執(zhí)行,那么該圖就是以 main 進程頂點開始的一棵根樹。在這個有向圖中,根頂點可達的對象都是有效對象,GC將不回收這些對象。如果某個對象 (連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那么我們認為這個(這些)對象不再被引用,可以被 GC 回收。
以下,我們舉一個例子說明如何用有向圖表示內(nèi)存管理。對于程序的每一個時刻,我們都有一個有向圖表示JVM的內(nèi)存分配情況。以下右圖,就是左邊程序運行到第6行的示意圖。

Java使用有向圖的方式進行內(nèi)存管理,可以消除引用循環(huán)的問題,例如有三個對象,相互引用,只要它們和根進程不可達的,那么GC也是可以回收它們的。這種方式的優(yōu)點是管理內(nèi)存的精度很高,但是效率較低。另外一種常用的內(nèi)存管理技術(shù)是使用計數(shù)器,例如COM模型采用計數(shù)器方式管理構(gòu)件,它與有向圖相比,精度行低(很難處理循環(huán)引用的問題),但執(zhí)行效率很高。
四、什么是Java中的內(nèi)存泄露
在Java中,內(nèi)存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點,首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;其次,這些對象是無用的,即程序以后不會再使用這些對象。如果對象滿足這兩個條件,這些對象就可以判定為Java中的內(nèi)存泄漏,這些對象不會被GC所回收,然而它卻占用內(nèi)存。
對于程序員來說,GC基本是透明的,不可見的。雖然,我們只有幾個函數(shù)可以訪問GC,例如運行GC的函數(shù)System.gc(),但是根據(jù)Java語言規(guī)范定義, 該函數(shù)不保證JVM的垃圾收集器一定會執(zhí)行。因為,不同的JVM實現(xiàn)者可能使用不同的算法管理GC。通常,GC的線程的優(yōu)先級別較低。JVM調(diào)用GC的策略也有很多種,有的是內(nèi)存使用到達一定程度時,GC才開始工作,也有定時執(zhí)行的,有的是平緩執(zhí)行GC,有的是中斷式執(zhí)行GC。但通常來說,我們不需要關(guān)心這些。除非在一些特定的場合,GC的執(zhí)行影響應(yīng)用程序的性能,例如對于基于Web的實時系統(tǒng),如網(wǎng)絡(luò)游戲等,用戶不希望GC突然中斷應(yīng)用程序執(zhí)行而進行垃圾回收,那么我們需要調(diào)整GC的參數(shù),讓GC能夠通過平緩的方式釋放內(nèi)存,例如將垃圾回收分解為一系列的小步驟執(zhí)行,Sun提供的HotSpot JVM就支持這一特性。
Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
在這個例子中,我們循環(huán)申請Object對象,并將所申請的對象放入一個 Vector 中,如果我們僅僅釋放引用本身,那么 Vector 仍然引用該對象,所以這個對象對 GC 來說是不可回收的。因此,如果對象加入到Vector 后,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 對象設(shè)置為 null。
五、內(nèi)存泄漏解決方法
1、全局集合類強引用造成的內(nèi)存泄漏(特別是 static 修飾的集合)
(1)靜態(tài)集合類引起內(nèi)存泄漏:
像HashMap、Vector等的使用最容易出現(xiàn)內(nèi)存泄露,這些靜態(tài)變量的生命周期和應(yīng)用程序一致,他們所引用的所有的對象Object也不能被釋放,因為他們也將一直被Vector等引用著。
Static Vector v = new Vector(10);
for (int i = 1; i<100; i++)
{
Object o = new Object();
v.add(o);
o = null;
}
在這個例子中,循環(huán)申請Object 對象,并將所申請的對象放入一個Vector 中,如果僅僅釋放引用本身(o=null),那么Vector 仍然引用該對象,所以這個對象對GC 來說是不可回收的。因此,如果對象加入到Vector 后,還必須從Vector 中刪除,最簡單的方法就是將Vector對象設(shè)置為null。
(2)當集合里面的對象屬性被修改后,再調(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元素對應(yīng)的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);
}
}
2、接收器、監(jiān)聽器注冊沒取消造成的內(nèi)存泄漏,如廣播,eventsbus等
在java 編程中,我們都需要和監(jiān)聽器打交道,通常一個應(yīng)用當中會用到很多監(jiān)聽器,我們會調(diào)用一個控件的諸如addXXXListener()等方法來增加監(jiān)聽器,但往往在釋放對象的時候卻沒有記住去刪除這些監(jiān)聽器,從而增加了內(nèi)存泄漏的機會。
3、各種連接沒有及時關(guān)閉造成的內(nèi)存泄漏
比如數(shù)據(jù)庫連接(dataSourse.getConnection()),網(wǎng)絡(luò)連接(socket)和io連接,除非其顯式的調(diào)用了其close()方法將其連接關(guān)閉,否則是不會自動被GC 回收的。對于Resultset 和Statement 對象可以不進行顯式回收,但Connection 一定要顯式回收,因為Connection 在任何時候都無法自動回收,而Connection一旦回收,Resultset 和Statement 對象就會立即為NULL。但是如果使用連接池,情況就不一樣了,除了要顯式地關(guān)閉連接,還必須顯式地關(guān)閉Resultset Statement 對象(關(guān)閉其中一個,另外一個也會關(guān)閉),否則就會造成大量的Statement 對象無法釋放,從而引起內(nèi)存泄漏。這種情況下一般都會在try里面去的連接,在finally里面釋放連接。
4、單例造成的內(nèi)存泄漏
由于單例的靜態(tài)特性使得其生命周期跟應(yī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;
}
}
這是一個普通的單例模式,當創(chuàng)建這個單例的時候,由于需要傳入一個Context,所以這個Context的生命周期的長短至關(guān)重要:
1、如果此時傳入的是 Application 的 Context,因為 Application 的生命周期就是整個應(yīng)用的生命周期,所以這將沒有任何問題。
2、如果此時傳入的是 Activity 的 Context,當這個 Context 所對應(yīng)的 Activity 退出時,由于該 Context 的引用被單例對象所持有,其生命周期等于整個應(yīng)用程序的生命周期,所以當前 Activity 退出時它的內(nèi)存并不會被回收,這就造成泄漏了。
正確的方式應(yīng)該改為下面這種方式:
public class AppManager {
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();// 使用Application 的context
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}
}
5、匿名內(nèi)部類/非靜態(tài)內(nèi)部類和異步線程持有生命周期短的引用導(dǎo)致的內(nèi)存泄漏(比如非靜態(tài)handler內(nèi)部類持有外部類Activity的引用)
匿名內(nèi)部類/非靜態(tài)內(nèi)部類和異步線程
有的時候我們可能會在啟動頻繁的Activity中,為了避免重復(fù)創(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ù),這樣雖然避免了資源的重復(fù)創(chuàng)建,不過這種寫法卻會造成內(nèi)存泄漏,因為非靜態(tài)內(nèi)部類默認會持有外部類的引用,而該非靜態(tài)內(nèi)部類又創(chuàng)建了一個靜態(tài)的實例,該實例的生命周期和應(yīng)用的一樣長,這就導(dǎo)致了該靜態(tài)實例一直會持有該Activity的引用,導(dǎo)致Activity的內(nèi)存資源不能正?;厥铡U_的做法為:
將該內(nèi)部類設(shè)為靜態(tài)內(nèi)部類或?qū)⒃搩?nèi)部類抽取出來封裝成一個單例,如果需要使用Context,請按照上面推薦的使用Application 的 Context。
匿名內(nèi)部類
android開發(fā)經(jīng)常會繼承實現(xiàn)Activity/Fragment/View,此時如果你使用了匿名類,并被異步線程持有了,那要小心了,如果沒有任何措施這樣一定會導(dǎo)致泄露
public class MainActivity extends Activity {
...
Runnable ref1 = new MyRunable();
Runnable ref2 = new Runnable() {
@Override
public void run() {
}
};
...
}

可以看到,ref1沒什么特別的。
但ref2這個匿名類的實現(xiàn)對象里面多了一個引用:
this$0這個引用指向MainActivity.this,也就是說當前的MainActivity實例會被ref2持有,如果將這個引用再傳入一個異步線程,此線程和此Acitivity生命周期不一致的時候,就造成了Activity的泄露。
其實匿名內(nèi)部類、非靜態(tài)內(nèi)部類和異步線程會造成內(nèi)存泄漏,原因都可以歸納為非靜態(tài)內(nèi)部類或異步線程持有了生命周期短的引用,如果生命周期短的對象被回收,持有該引用的對象就不會被gc回收
handler造成的內(nèi)存泄漏就是一個很好的例子
可以看另外一篇文章
自定義 Handler 時如何有效地避免內(nèi)存泄漏問題
六、總結(jié)
對 Activity 等組件的引用應(yīng)該控制在 Activity 的生命周期之內(nèi); 如果不能就考慮使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部長生命周期的對象引用而泄露。
盡量不要在靜態(tài)變量或者靜態(tài)內(nèi)部類中使用非靜態(tài)外部成員變量(包括context ),即使要使用,也要考慮適時把外部成員變量置空;也可以在內(nèi)部類中使用弱引用來引用外部類的變量。
對于生命周期比Activity長的內(nèi)部類對象,并且內(nèi)部類中使用了外部類的成員變量,可以這樣做避免內(nèi)存泄漏:
將內(nèi)部類改為靜態(tài)內(nèi)部類
靜態(tài)內(nèi)部類中使用弱引用來引用外部類的成員變量
Handler 的持有的引用對象最好使用弱引用,資源釋放時也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的時候,取消掉該 Handler 對象的 Message和 Runnable.
在 Java 的實現(xiàn)過程中,也要考慮其對象釋放,最好的方法是在不使用某對象時,顯式地將此對象賦值為 null,比如使用完Bitmap 后先調(diào)用 recycle(),再賦為null,清空對圖片等資源有直接引用或者間接引用的數(shù)組(使用 array.clear() ; array = null)等,最好遵循誰創(chuàng)建誰釋放的原則。
正確關(guān)閉資源,對于使用了BraodcastReceiver,ContentObserver,F(xiàn)ile,游標 Cursor,Stream,Bitmap等資源的使用,應(yīng)該在Activity銷毀時及時關(guān)閉或者注銷。
保持對對象生命周期的敏感,特別注意單例、靜態(tài)對象、全局性集合等的生命周期。
參考:
https://blog.csdn.net/wtt945482445/article/details/52483944