技術帖:如何解決Java的頭號殺手—內存洩露!

編程語言 數碼 IT168企業級 IT168企業級 2017-08-02

筆者曾經聽到同事在一次會議發表如下聲明:如果你是Android開發人員,並且你不使用WeakReferences,那你就會有麻煩。

毫無疑問,有人可能會爭論WeakReferences是否真的這麼重要,但這其中隱藏著Java領域最大的問題之一:內存洩漏。

內存洩漏

內存洩漏是無聲的殺手,它們在初始孵化期間慢慢滋生,沒有任何人發現。隨著時間推移,它們不斷成長、堆積和積累。當你意識到它們的存在時已經太遲:你的整個代碼庫充滿內存洩漏,尋找解決方案需要付出巨大努力。因此,我們應該儘可能在早期階段發現內存洩漏。

讓我們從頭開始。

什麼是內存洩漏?

當不再使用的對象仍被其他對象引用到內存時,便會發生內存洩漏。這在Android世界特別麻煩,因為Android設備內存量非常有限,有時候只有16MB。你可能認為這足以運行應用程序,但請相信我:你很快會超越這個限制。

吃掉可用內存是最直接的結果,但低內存還有一個有趣的副作用:垃圾收集器(GC)將開始更頻繁地觸發。當GC觸發時,世界停止。應用需要每隔16毫秒渲染一幀,而隨著GC開始運行,此幀速率可能會受到影響。

洩漏如何發生?

那麼,你是如何讓內存洩漏發生的呢?讓我們看看我們如何可引起內存洩漏:

  • 沒有關閉開放流。我們通常打開數據流連接到數據庫池、到開放網絡連接或者開始讀取文件,沒有關閉它們會造成內存洩漏。

  • 使用靜態字段來保存引用。靜態對象總是在內存中,如果你聲明太多靜態字段來保存引用得到對象,這會造成內存洩漏。對象越大,內存洩漏就越大。

  • 利用不正確使用hashCode() (或根本不用)或equals()的HashSet。這樣的話,HashSet會開始變大,對象將被重複插入!實際上,當筆者被問及“HashSet中hasCode()和equals()的目的是什麼”時,筆者總是有相同的答案:避免啊內存洩漏!也許這不是最務實的答案,但確實如此。

如果你是Android開發人員,內存洩漏的可能性會呈指數級增長。Context對象主要用於訪問和加載不同的資源,它作為參數被傳遞給很多類和方法。

想像一下旋轉屏幕的情況。在這種情況中,Android會破壞當前活動,並師徒在旋轉發生之前重新創建相同的狀態。在很多情況下,如果我們假設你不想重新加載長的Bitmap,你需要保持靜態引用,以避免Bitmap被重新加載。這個問題是,Bitmap通常在Drawable實例化,它最終與其他元素鏈接,並被鏈接到Context級別,洩漏整個類。這也是為什麼應該非常小心靜態類的原因之一。

我們如何避免內存洩漏?

還記得我們前面討論過WeakReferences?讓我們看看Java中不同類型的引用:

  • Normal:這是主要參考類型。它對應於對象的簡單創建,當該對象不再使用和引用時將會被收集。這只是典型的對象實例化: SampleObject sampleObject = new SampleObject();

  • Soft:這是個不夠強大單獨引用,當垃圾收集器事件觸發時它無法將對象保存在內存中。因此在執行期間的任何時候它可為空。通過使用這個引用,垃圾收集器會根據系統需求決定何時釋放對象內存。為了使用該引用,只要創建一個SoftReference對象將實體對象作為參數傳遞給構造函數,並調用SoftReference.get()來獲取對象: SoftReference<SampleObject> sampleObjectSoftRef = new SoftReference<SampleObject>(new SampleObject()); SampleObject sampleObject = sampleObjectSoftRef.get();

  • Weak:這就像SoftReference,但更弱;

  • Phantom:這是最弱的引用;該對象可用於析構。這種類型的引用很少被使用, PhantomReference.get()方法通常返回null,這個引用目前我們不感興趣,但知道這種引用也是有用單獨。

如果我們知道哪些對象有較低的優先級以及可被收集而不會在應用程序正常執行中引起問題,這些類可能會有用,讓我們來看看如何使用它們:技術帖:如何解決Java的頭號殺手—內存洩露!

非靜態內部類被廣泛用於Android,因為它們允許我們訪問外啊不類的ID,而不直接傳遞它們的引用。然而,Android開發人員通常會添加內部類來節省時間,而沒有意識到對內存性能的影響。當Activity啟動時,簡單單獨AsyncTask被創建並執行。但內部類需要可訪問外部類,所以每次Activity被破壞時都會發生內存洩漏,但AsyncTask仍然工作。這不僅在調用Activity.finish()時發生,當因為配置變更或內存需求Activity被系統強制破壞時也會發生,並且,它會被再次創建。AsyncTask保存對每個Activity的引用,使其在銷燬時無法被垃圾收集。

考慮一下,當用戶在任務運行時旋轉設備會發生什麼情況:整個Activity實例需要始終可用知道AsyncTask完成。此外,大部分時候我們想要AsyncTask使用AsyncTask.onPostExecute()方法將結果顯示在屏幕。這可能導致崩潰,因為當任務仍然在運行時Activity被破壞,查看引用可能為空。

那麼,解決方案是什麼呢?如果我們將內部類設置為靜態,我們無法訪問外部類,所以我們需要提供引用。。為了增加兩個實例之間的距離,以及讓垃圾收集器正常配合Activity工作,讓我們使用更弱的引用來實現更清晰的內存管理,前面的代碼更改為:

技術帖:如何解決Java的頭號殺手—內存洩露!

這樣的話,這些類被分離,Activity不再使用時將被收集,AsyncTask對象不會在WeakReferences對象內找到Activity實例,也不會執行AsyncTask.onPostExecute()方法代碼。

通過正確使用引用,我們可使用這些方法來避免在代碼中引發內存洩漏:

  • 避免在Activity中使用非靜態內部類,使用靜態內部類並進行WeakReferences

  • 當你可選擇使用Context時,嘗試使用Activity Context而不是Application Context

  • 一般來說,永遠不要引用到任何類型的Context

相關推薦

推薦中...