繁体   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