1. 程式人生 > 實用技巧 >Java記憶體洩漏與野指標問題

Java記憶體洩漏與野指標問題

四種引用型別的介紹
強引用(StrongReference):JVM 寧可丟擲 OOM ,也不會讓 GC 回收具有強引用的物件;

軟引用(SoftReference):只有在記憶體空間不足時,才會被回的物件;

弱引用(WeakReference):在 GC 時,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體;

虛引用(PhantomReference):任何時候都可以被GC回收,當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否存在該物件的虛引用,來了解這個物件是否將要被回收。可以用來作為GC回收Object的標誌。

我們常說的記憶體洩漏是指new出來的Object無法被GC回收,即為強引用:  

什麼是Java中的記憶體洩露
對於C++來說,記憶體洩漏就是new出來的物件沒有delete,俗稱野指標;對於Java來說,就是new出來的Object 放在Heap上無法被GC回收;  

在Java中,記憶體洩漏就是存在一些被分配的物件,這些物件有下面兩個特點,首先,這些物件是可達的,即在有向圖中,存在通路可以與其相連;其次,這些物件是無用的,即程式以後不會再使用這些物件。如果物件滿足這兩個條件,這些物件就可以判定為Java中的記憶體洩漏,這些物件不會被GC所回收,然而它卻佔用記憶體。

在C++中,記憶體洩漏的範圍更大一些。有些物件被分配了記憶體空間,然後卻不可達,由於C++中沒有GC,這些記憶體將永遠收不回來。在Java中,這些不可達的物件都由GC負責回收,因此程式設計師不需要考慮這部分的記憶體洩露。

通過分析,我們得知,對於C++,程式設計師需要自己管理邊和頂點,而對於Java程式設計師只需要管理邊就可以了(不需要管理頂點的釋放)。通過這種方式,Java提高了程式設計的效率。

因此,通過以上分析,我們知道在Java中也有記憶體洩漏,但範圍比C++要小一些。因為Java從語言上保證,任何物件都是可達的,所有的不可達物件都由GC管理。

對於程式設計師來說,GC基本是透明的,不可見的。雖然,我們只有幾個函式可以訪問GC,例如執行GC的函式System.gc(),但是根據Java語言規範定義, 該函式不保證JVM的垃圾收集器一定會執行。因為,不同的JVM實現者可能使用不同的演算法管理GC。通常,GC的執行緒的優先級別較低。JVM呼叫GC的策略也有很多種,有的是記憶體使用到達一定程度時,GC才開始工作,也有定時執行的,有的是平緩執行GC,有的是中斷式執行GC。但通常來說,我們不需要關心這些。除非在一些特定的場合,GC的執行影響應用程式的效能,例如對於基於Web的實時系統,如網路遊戲等,使用者不希望GC突然中斷應用程式執行而進行垃圾回收,那麼我們需要調整GC的引數,讓GC能夠通過平緩的方式釋放記憶體,例如將垃圾回收分解為一系列的小步驟執行,Sun提供的HotSpot JVM就支援這一特性。

同樣給出一個 Java 記憶體洩漏的典型例子,

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
Object o = new Object();
v.add(o);
o = null;
}
在這個例子中,我們迴圈申請Object物件,並將所申請的物件放入一個 Vector 中,如果我們僅僅釋放引用本身,那麼 Vector 仍然引用該物件,所以這個物件對 GC 來說是不可回收的。因此,如果物件加入到Vector 後,還必須從 Vector 中刪除,最簡單的方法就是將 Vector 物件設定為 null。

Android中常見的記憶體洩漏彙總
什麼是記憶體溢位:(Out Of Memory,簡稱 OOM),通俗理解就是記憶體不夠,即記憶體佔用超出記憶體的空間大小;基本上會造成程式奔潰。解決:找到報錯的程式碼並解決。
什麼是記憶體洩露:記憶體洩漏(Memory Leak),簡單理解就是記憶體使用完畢之後本該垃圾回收卻未被回收。(佔著茅坑不拉屎)--記憶體無法回收又沒起作用。

推理!!!
1.看物件的大小;
2.深入使用MAT進行物件細節的分析
3.前後對比。
懷疑某一段程式碼會造成記憶體溢位。可以先註冊掉該段程式碼生成一個xxx.hroph檔案,然後在解開這段程式碼再生成一個yyy.hroph檔案
通過MAT工具對比他們的記憶體消耗情況---甚至可以精確到每一個物件消耗記憶體的情況。

在 Android 中,洩露 Context 物件的問題尤其嚴重,特別像 Activity 這樣的 Context 物件會引用大量很佔用記憶體的物件,如果 Context 物件發生了記憶體洩漏,那它所引用的所有物件都被洩漏了。Activity 是非常重量級的物件,所以我們應該極力避免妨礙系統對其進行回收,然而實際情況是有多種方式會無意間就洩露了Activity 物件。

case 0.靜態變數造成的記憶體洩漏

最簡單的洩漏 Activity 就是在 Activity 類中定義一個 static 變數,並將其指向一個執行中的 Activity 例項。如果在 Activity 的生命週期結束之前,沒有清除這個引用,那它就會洩漏。由於 Activity 的類物件是靜態的,一旦載入,就會在 APP 執行時一直常駐記憶體,如果類物件不解除安裝,其靜態成員就不會被垃圾回收。

儘量避免使用 static 成員變數:
這裡修復的方法是:
不要在類初始時初始化靜態成員。可以考慮lazy初始化。

case 1. 單例造成的記憶體洩露
單例的靜態特性導致其生命週期同應用一樣長

另一種類似的情況是對經常啟動的 Activity 實現一個單例模式,讓其常駐記憶體可以使它能夠快速恢復狀態。

如我們有一個建立起來非常耗時的 View,在同一個 Activity 不同的生命週期中都保持不變呢,就為它實現一個單例模式。一旦 View 被載入到介面中,它就會持有 Context 的強引用,也就是我們的 Activity 物件。

由於我們是通過一個靜態成員引用了這個 View,所以我們也就引用了 Activity,因此 Activity 就發生了洩漏。所以一定不要把載入的 View 賦值給靜態變數,如果你真的需要,那一定要確保在 Activity 銷燬之前將其從 View 層級中移除。

解決方案:

將該屬性的引用方式改為弱引用;

如果傳入Context,使用ApplicationContext;

case 2. InnerClass匿名內部類
我們經常在 Activity 內部定義一個內部類,這樣做可以增加封裝性和可讀性。但是如果當我們建立了一個內部類的物件,並通過靜態變數持有了 Activity 的引用,那也會可能發生 Activity 洩漏。

在Java中,非靜態內部類 和 匿名類 都會潛在的引用它們所屬的外部類,但是,靜態內部類卻不會。如果這個非靜態內部類例項做了一些耗時的操作,就會造成外圍物件不會被回收,從而導致記憶體洩漏。

解決方案:

將內部類變成靜態內部類;

靜態內部類中使用弱引用來引用外部類的成員變數;

如果有強引用Activity中的屬性,則將該屬性的引用方式改為弱引用;

在業務允許的情況下,當Activity執行onDestory時,結束這些耗時任務;

case 3. 執行緒造成的記憶體洩漏
在 Activity 內定義了一個匿名的 AsyncTask 物件,就有可能發生記憶體洩漏。如果 Activity 被銷燬之後 AsyncTask 仍然在執行,那就會阻止垃圾回收器回收Activity 物件,進而導致記憶體洩漏,直到執行結束才能回收 Activity。

同樣的,使用 Thread 和 TimerTask 也可能導致 Activity 洩漏。只要它們是通過匿名類建立的,儘管它們在單獨的執行緒被執行,它們也會持有對 Activity 的強引用,進而導致記憶體洩漏。

在Android裡面執行緒最容易造成記憶體洩露。執行緒產生記憶體洩露的主要原因在於執行緒生命週期的不可控
2.執行緒問題的改進方式主要有:
1)將執行緒的內部類,改為靜態內部類。
2)在程式中儘量採用弱引用儲存Context。

case 4. Activity Context 的不正確使用
在Android應用程式中通常可以使用兩種Context物件:Activity和Application。當類或方法需要Context物件的時候常見的做法是使用第一個作為Context引數。這樣就意味著View物件對整個Activity保持引用,因此也就保持對Activty的所有的引用。

假設一個場景,當應用程式有個比較大的Bitmap型別的圖片,每次旋轉是都重新載入圖片所用的時間較多。為了提高螢幕旋轉是Activity的建立速度,最簡單的方法時將這個Bitmap物件使用Static修飾。 當一個Drawable繫結在View上,實際上這個View物件就會成為這份Drawable的一個Callback成員變數。而靜態變數的生命週期要長於Activity。導致了當旋轉螢幕時,Activity無法被回收,而造成記憶體洩露。

解決方案:

使用ApplicationContext代替ActivityContext,因為ApplicationContext會隨著應用程式的存在而存在,而不依賴於activity的生命週期;

對Context的引用不要超過它本身的生命週期,慎重的對Context使用“static”關鍵字。Context裡如果有執行緒,一定要在onDestroy()裡及時停掉。

case 5. Handler引起的記憶體洩漏
定義一個匿名的 Runnable 物件並將其提交到 Handler 上也可能導致 Activity 洩漏。Runnable 物件間接地引用了定義它的 Activity 物件,而它會被提交到Handler 的 MessageQueue 中,如果它在 Activity 銷燬時還沒有被處理,就會導致 Activity 洩漏。

當Handler中有延遲的的任務或是等待執行的任務佇列過長,由於訊息持有對Handler的引用,而Handler又持有對其外部類的潛在引用,這條引用關係會一直保持到訊息得到處理,而導致了Activity無法被垃圾回收器回收,而導致了記憶體洩露。

修復方法:在 Activity 中避免使用非靜態內部類,比如上面我們將 Handler 宣告為靜態的,則其存活期跟 Activity 的生命週期就無關了。同時通過弱引用的方式引入 Activity,避免直接將 Activity 作為 context 傳進去,見下面程式碼:

Handler 的持有的引用物件最好使用弱引用,資源釋放時也可以清空 Handler 裡面的訊息。比如在 Activity onStop 或者 onDestroy 的時候,取消掉該 Handler 物件的 Message和 Runnable.
綜述,即推薦使用靜態內部類 + WeakReference 這種方式。每次使用前注意判空。

解決方案:

可以把Handler類放在單獨的類檔案中,或者使用靜態內部類便可以避免洩露;

如果想在Handler內部去呼叫所在的Activity,那麼可以在handler內部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式來達到斷開Handler與Activity之間存在引用關係的目的。

case 6. 註冊監聽器的洩漏(資源未關閉造成的記憶體洩漏)
如系統服務可以通過 context.getSystemService 獲取,它們負責執行某些後臺任務,或者為硬體訪問提供介面。如果 Context 物件想要在服務內部的事件發生時被通知,那就需要把自己註冊到服務的監聽器中。然而,這會讓服務持有 Activity 的引用,如果開發者忘記在 Activity 銷燬時取消註冊,也會導致 Activity洩漏

系統服務可以通過Context.getSystemService 獲取,它們負責執行某些後臺任務,或者為硬體訪問提供介面。如果Context 物件想要在服務內部的事件發生時被通知,那就需要把自己註冊到服務的監聽器中。然而,這會讓服務持有Activity 的引用,如果在Activity onDestory時沒有釋放掉引用就會記憶體洩漏。

解決方案:

使用ApplicationContext代替ActivityContext;

在Activity執行onDestory時,呼叫反註冊;

case 7. Cursor,Stream沒有close,View沒有recyle
資源性物件比如(Cursor,File檔案等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收記憶體。它們的緩衝不僅存在於 java虛擬機器內,還存在於java虛擬機器外。如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體洩漏。因為有些資源性物件,比如SQLiteCursor(在解構函式finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性物件在不使用的時候,應該呼叫它的close()函式,將其關閉掉,然後才置為null. 在我們的程式退出時一定要確保我們的資源性物件已經關閉。

對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出,否則這些資源將不會被回收,造成記憶體洩漏。

Solution:

呼叫onRecycled()

case 8. 集合中物件沒清理造成的記憶體洩漏
我們通常把一些物件的引用加入到了集合容器(比如ArrayList)中,當我們不需要該物件時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。
所以要在退出程式之前,將集合裡的東西clear,然後置為null,再退出程式。

解決方案:

在Activity退出之前,將集合裡的東西clear,然後置為null,再退出程式。

Solution

private List data;
public void onDestory() {
if (data != null) {
data.clear();
data = null;
}
}
case 9. WebView造成的洩露
當我們不要使用WebView物件時,應該呼叫它的destory()函式來銷燬它,並釋放其佔用的記憶體,否則其佔用的記憶體長期也不能被回收,從而造成記憶體洩露。

解決方案:

為webView開啟另外一個程序,通過AIDL與主執行緒進行通訊,WebView所在的程序可以根據業務的需要選擇合適的時機進行銷燬,從而達到記憶體的完整釋放。

case 10. 構造Adapter時,沒有使用快取的ConvertView
初始時ListView會從Adapter中根據當前的屏幕布局例項化一定數量的View物件,同時ListView會將這些View物件 快取起來。
當向上滾動ListView時,原先位於最上面的List Item的View物件會被回收,然後被用來構造新出現的最下面的List Item。
這個構造過程就是由getView()方法完成的,getView()的第二個形參View ConvertView就是被快取起來的List Item的View物件(初始化時快取中沒有View物件則ConvertView是null)。

case 11.動畫

在屬性動畫中有一類無限迴圈動畫,如果在Activity中播放這類動畫並且在onDestroy中去停止動畫,那麼這個動畫將會一直播放下去,這時候Activity會被View所持有,從而導致Activity無法被釋放。解決此類問題則是需要早Activity中onDestroy去去呼叫objectAnimator.cancel()來停止動畫

case 12.第三方庫使用不當

對於EventBus,RxJava等一些第三開源框架的使用,若是在Activity銷燬之前沒有進行解除訂閱將會導致記憶體洩漏。

綜上所述,要避免記憶體洩露或者記憶體溢位,主要要遵循以下幾點:

第一:不要為Context長期儲存引用(要引用Context就要使得引用物件和它本身的生命週期保持一致,即對activity的引用應該控制在activity的生命週期之內)。

第二:如果要使用到Context,儘量使用ApplicationContext去代替Context,因為ApplicationContext的生命週期較長,引用情況下不會造成記憶體洩露問題

第三:在你不控制物件的生命週期的情況下避免在你的Activity中使用static變數。儘量使用WeakReference去代替一個static。

第四:垃圾回收器並不保證能準確回收記憶體,這樣在使用自己需要的內容時,主要生命週期和及時釋放掉不需要的物件。儘量在Activity的生命週期結束時,在onDestroy中把我們做引用的其他物件做資源釋放,比如:cursor.close()。如清空對圖片等資源有直接引用或者間接引用的陣列(使用array.clear();array = null);

第五:儘量不要在Activity中使用非靜態內部類,因為非靜態內部類會隱式持有外部類例項的引用。如果使用靜態內部類,將外部例項引用作為弱引用持有。
(靜態的內部類不會持有外部類的一個隱式引用)

在 Java 的實現過程中,也要考慮其物件釋放,最好的方法是在不使用某物件時,顯式地將此物件賦值為 null,比如使用完Bitmap 後先呼叫 recycle(),再賦為null,清空對圖片等資源有直接引用或者間接引用的陣列(使用 array.clear() ; array = null)等,最好遵循誰建立誰釋放的原則。
正確關閉資源,對於使用了BraodcastReceiver,ContentObserver,File,遊標 Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者登出。
保持對物件生命週期的敏感,特別注意單例、靜態物件、全域性性集合等的生命週期。

程式出現停止執行狀態或者致命崩潰現象可能如下原因導致:

1.在while死迴圈裡面或軟體操作非常頻繁的程式碼塊中進行new物件產生強引用物件,
導致jvm不能及時回收記憶體,從而產生記憶體消耗暴增,讓軟體出現停止執行的致命崩潰現象

2.程式碼報錯未捕捉異常造成

3.程式在主執行緒耗時過長,或者在廣播中的耗時超過6s

new出來的物件不用時回收原則;

1.在哪建立就在哪及時釋放,
2.誰引用,誰就負責釋放
能做到C/C++對於程式的“誰建立,誰釋放”原則,那我們對於記憶體的把握,並不比Java或Android本身的GC機制差,而且更好的控制記憶體,能使我們的手機執行得更流暢

java沒有絕對的強制垃圾回收的方法,不過可以這樣去做:

  1. 對於不再引用的物件,及時把它的引用賦為null。 obj = null;
  2. 如果記憶體確實很緊張,呼叫System.gc() 方法來建議垃圾回收器開始回收垃圾