簡體   English   中英

垃圾收集器沒有像在 Android 應用程序中那樣釋放“垃圾內存”

[英]Garbage Collector not freeing "trash memory" as it should in an Android application

你好!

我是一名初級 Java 和 Android 開發人員,最近在處理我的應用程序的內存管理方面遇到了麻煩。 我將把這篇文章分成幾個部分,以使其更清晰易讀。


我的應用程序的簡要說明

這是一個由幾個階段(級別)組成的游戲。 每個階段都有一個玩家的起點和一個出口,引導玩家進入下一個階段。 每個階段都有自己的一套障礙。 目前,當玩家到達最后階段(我目前只創建了 4 個)時,他/她會自動回到第一階段(1 級)。

一個名為GameObject (擴展Android.View )的抽象類定義了玩家和游戲中存在的所有其他對象(障礙物等)的基本結構和行為。 所有對象(本質上是視圖)都繪制在我創建的自定義視圖中(擴展 FrameLayout)。 游戲邏輯和游戲循環由一個側線程(gameThread)處理。 這些階段是通過從 xml 文件中檢索元數據來創建的。

問題

除了我的代碼中所有可能的內存泄漏(我一直在努力尋找和解決所有這些問題)之外,還有一個與垃圾收集器發生相關的奇怪現象。 我將使用圖像,而不是用文字來描述它並冒着讓您感到困惑的風險。 孔子說:“千言萬語”。 好吧,在這種情況下,我剛剛讓您免於閱讀 150,000 個單詞,因為我下面的 GIF 有 150 幀。

第一次加載時的第 1 階段。該應用程序的總內存分配為 85mb。 第二次加載時的第 1 階段。該應用程序的總內存分配為 130mb。在我強制執行 2 次垃圾回收(使用 Android Profiler)后,內存回到 85mb。

描述:第一張圖片代表我的應用程序在第一次加載“stage 1”時的內存使用情況。 第二張圖片 (GIF) 首先表示第二次加載“第 1 階段”時我的應用程序的內存使用時間線(發生這種情況,如前所述,當玩家擊敗最后一個階段時),然后是四個強制啟動的垃圾收集由我。

您可能已經注意到,這兩種情況在內存使用方面存在巨大差異(幾乎 50MB)。 當“第一階段”首次加載時,當游戲開始時,應用程序使用 85MB 的內存。 第二次加載同一個stage的時候,稍晚一點,內存使用量已經是130MB了! 這可能是由於我的一些糟糕的編碼,因此我不在這里。 你有沒有注意到,在我強行執行了 2 次(實際上是 4 次,但只有前 2 次重要)垃圾收集之后,內存使用情況又回到了它的“正常狀態”(與第一次加載舞台時的內存使用情況相同)? 這就是我所說的奇怪現象

問題

如果垃圾收集器應該從不再被引用的內存對象中刪除(或者至少只有弱引用),為什么你在上面看到的“垃圾內存”只有在我強行調用GC和不是關於GC的正常執行? 我的意思是,如果我手動啟動的垃圾收集可以刪除這個“thrash”,那么正常的GC執行也可以刪除它。 為什么沒有發生?

我什至嘗試在切換階段時調用System.gc() ,但是,即使發生了垃圾收集,也不會像我手動執行GC那樣刪除這個“thrash”內存。 我是否遺漏了一些關於垃圾收集器如何工作或 Android 如何實現它的重要信息?

最后的考慮

我花了幾天時間搜索、研究和修改我的代碼,但我找不到為什么會這樣。 StackOverflow 是我最后的選擇。 謝謝!

注意:我打算發布一些可能與我的應用程序源代碼相關的部分,但由於問題已經太長了,我將在這里停止。 如果您覺得需要檢查某些代碼,請告訴我,我將編輯此問題。

我已經讀過的:
如何在 Java 中強制垃圾收集?
Android中的垃圾收集器
Oracle 的 Java 垃圾收集基礎
Android 內存概覽
Android 中的內存泄漏模式
避免 Android 中的內存泄漏
管理您的應用程序的內存
您需要了解的有關 Android 應用程序內存泄漏的信息
使用 Memory Profiler 查看 Java 堆和內存分配
LeakCanary(Android 和 Java 內存泄漏檢測庫)
Android 內存泄漏和垃圾收集
通用 Android 垃圾收集
如何從內存中清除動態創建的視圖?
引用如何在 Android 和 Java 中工作
Java 垃圾收集器 - 無法定期正常運行
android中的垃圾收集(手動完成)
......還有更多我再也找不到了。

垃圾回收比較復雜,不同平台實現方式不同。 事實上,同一平台的不同版本實現垃圾收集的方式不同。 (和更多 ... )

一個典型的現代收藏家是基於對大多數物品年輕時死去的觀察; 即它們在創建后很快就無法訪問。 然后將堆分成兩個或多個“空間”; 例如“年輕”空間和“舊”空間。

  • “年輕”空間是創建新對象的地方,它經常被收集。 “年輕”的空間往往更小,“年輕”的收藏發生得很快。
  • “舊”空間是長期存在的對象結束的地方,它很少被收集。 在“舊”空間收藏往往更貴。 (出於各種原因。)
  • 在“新”空間中存活多次 GC 周期的對象將獲得“終身使用權”; 即它們被移動到“舊”空間。
  • 偶爾我們可能會發現需要同時收集新舊空間。 這稱為完整集合。 完整的 GC 是最昂貴的,並且通常會在相對較長的時間內“停止世界”。

(還有各種其他巧妙而復雜的東西……我不會深入討論。)


您的問題是為什么在您調用System.gc()之前空間使用量不會顯着下降。

答案基本上是,這是做事的有效方式。

收集的真正目標不是一直釋放盡可能多的內存。 相反,目標是確保在需要時有足夠的空閑內存,並以最小的 CPU 開銷或最小的 GC 暫停來做到這一點。

因此,在正常操作中,GC 的行為將如上:頻繁地進行“新”空間收集和不頻繁地進行“舊”空間收集。 並且集合將“根據需要”運行。

但是當您調用System.gc() ,JVM通常會嘗試取回盡可能多的內存。 這意味着它會執行“完整的 gc”。

現在我想你說過需要幾次System.gc()調用才能產生真正的不同,這可能與使用finalize方法或Reference對象或類似方法有關。 事實證明,在主 GC 完成后,后台線程會處理可終結對象和Reference 對象實際上只是處於可以收集和刪除之后的狀態 所以需要另一個 GC 來最終擺脫它們。

最后,還有整體堆大小的問題。 當堆太小時,大多數 VM 會從主機操作系統請求內存,但不願意將其歸還。 Oracle 收集器會記錄連續“完整”收集結束時的可用空間比率。 如果在多次 GC 周期后可用空間比率“太高”,它們只會減小堆的整體大小。 Oracle GC 采用這種方法的原因有很多:

  1. 當垃圾對象與非垃圾對象的比率很高時,典型的現代 GC 工作效率最高。 因此,保持堆大有助於提高效率。

  2. 應用程序的內存需求很有可能再次增長。 但是 GC 需要運行才能檢測到。

  3. JVM 反復將內存返還給操作系統並重新請求它,這可能會破壞操作系統虛擬內存算法。

  4. 如果操作系統缺少內存資源,就會出現問題; 例如 JVM:“我不需要這個內存。把它拿回來”,操作系統:“謝謝”,JVM:“哦……我又需要它了!”,操作系統:“不”,JVM:“OOME”。

假設 Android 收集器的工作方式相同,這就是為什么必須多次運行System.gc()才能縮小堆大小的另一種解釋。


在開始向代碼添加System.gc()調用之前,請閱讀為什么調用 System.gc() 是不好的做法? .

我在我的應用程序上遇到了同樣的問題,我看到您已經了解 GC,請嘗試觀看此視頻,了解為什么需要 GC。 嘗試將此代碼添加到您的應用程序類(應用程序的java文件,就像每個活動的每個java文件一樣)並將此代碼添加到“onCreate”的覆蓋下(代碼在kotlin中)

這是洞類:

open class _appName_() : Application(){
    private var appKilled = false
    override fun onCreate() {
        super.onCreate()
        thread {
            while (!appKilled){
                Thread.sleep(6000)
                System.runFinalization()
                Runtime.getRuntime().gc()
                System.gc()
            }
        }
    }
    override fun onTerminate() {
        super.onTerminate()
        appKilled = true
    }
}

這段代碼使得每 6 秒 GC 被調用一次

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM