简体   繁体   English

垃圾收集器没有像在 Android 应用程序中那样释放“垃圾内存”

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

Hello!你好!

I'm a beginner Java and Android developer and I've been having trouble lately dealing with my app's memory management.我是一名初级 Java 和 Android 开发人员,最近在处理我的应用程序的内存管理方面遇到了麻烦。 I will break this text into sections, in order to make it clearer and readable.我将把这篇文章分成几个部分,以使其更清晰易读。


A brief description of my app我的应用程序的简要说明

It's a game that consists of several stages (levels).这是一个由几个阶段(级别)组成的游戏。 Each stage has a starting point for the player and an exit, which leads the player to the next stage.每个阶段都有一个玩家的起点和一个出口,引导玩家进入下一个阶段。 Each stage has its own set of obstacles.每个阶段都有自己的一套障碍。 Currently, when the player reaches the final stage (I've only created 4 so far) he/she automatically goes back to the first stage (level 1).目前,当玩家到达最后阶段(我目前只创建了 4 个)时,他/她会自动回到第一阶段(1 级)。

An abstract class called GameObject (extends Android.View ) defines the base structure and behaviour for the player and all the other objects (obstacles, etc) present in the game.一个名为GameObject (扩展Android.View )的抽象类定义了玩家和游戏中存在的所有其他对象(障碍物等)的基本结构和行为。 All the objects (that are, essentially, views) are drawn in a custom view created by me (extends FrameLayout).所有对象(本质上是视图)都绘制在我创建的自定义视图中(扩展 FrameLayout)。 The game logic and the game loop is handled by a side thread (gameThread).游戏逻辑和游戏循环由一个侧线程(gameThread)处理。 The stages are created by retrieving metadata from xml files.这些阶段是通过从 xml 文件中检索元数据来创建的。

The problem问题

Besides all the possible memory leaks on my code (all of which I've been working hard to find and solve), there is a strange phenomenon related to the garbage collector happening.除了我的代码中所有可能的内存泄漏(我一直在努力寻找和解决所有这些问题)之外,还有一个与垃圾收集器发生相关的奇怪现象。 Instead of describing it with words and risk getting you confused, I will use images.我将使用图像,而不是用文字来描述它并冒着让您感到困惑的风险。 As Confucius said, "An image is worth a thousand words".孔子说:“千言万语”。 Well, in this case, I've just saved you from reading 150,000 words, since my GIF below has 150 frames.好吧,在这种情况下,我刚刚让您免于阅读 150,000 个单词,因为我下面的 GIF 有 150 帧。

第一次加载时的第 1 阶段。该应用程序的总内存分配为 85mb。 第二次加载时的第 1 阶段。该应用程序的总内存分配为 130mb。在我强制执行 2 次垃圾回收(使用 Android Profiler)后,内存回到 85mb。

Description: the first image represents my app's memory usage when the "stage 1" is first loaded.描述:第一张图片代表我的应用程序在第一次加载“stage 1”时的内存使用情况。 The second image (GIF) firstly represents my app's memory usage timeline when the "stage 1" is loaded for the second time (this happens, as described earlier, when the player beat the last stage) and is followed by four garbage collections forcefully initiated by me.第二张图片 (GIF) 首先表示第二次加载“第 1 阶段”时我的应用程序的内存使用时间线(发生这种情况,如前所述,当玩家击败最后一个阶段时),然后是四个强制启动的垃圾收集由我。

As you might have noticed, there is a huge difference (almost 50MB) in the memory usage between the two situations.您可能已经注意到,这两种情况在内存使用方面存在巨大差异(几乎 50MB)。 When the "Stage 1" is firstly loaded, when the game starts, the app is using 85MB of memory.当“第一阶段”首次加载时,当游戏开始时,应用程序使用 85MB 的内存。 When the same stage is loaded for the second time, a little bit later, the memory usage is already at 130MB!第二次加载同一个stage的时候,稍晚一点,内存使用量已经是130MB了! That's probably due to some bad coding on my part and I'm not here because of this.这可能是由于我的一些糟糕的编码,因此我不在这里。 Have you noticed how, after I forcefully performed 2 (actually 4, but only the first 2 mattered) garbage collections, the memory usage went back to it's "normal state" (the same memory usage as when the stage was firstly loaded)?你有没有注意到,在我强行执行了 2 次(实际上是 4 次,但只有前 2 次重要)垃圾收集之后,内存使用情况又回到了它的“正常状态”(与第一次加载舞台时的内存使用情况相同)? That's the weird phenomenon I was talking about .这就是我所说的奇怪现象

The question问题

If the garbage collector is supposed to remove from memory objects that are no long being referenced (or, at least, have only weak references ), why is the "trash memory" that you saw above being removed only when I forcefully call the GC and not on the GC's normal executions?如果垃圾收集器应该从不再被引用的内存对象中删除(或者至少只有弱引用),为什么你在上面看到的“垃圾内存”只有在我强行调用GC和不是关于GC的正常执行? I mean, if the garbage collection manually initiated by me could remove this "thrash", then the normal GC's executions would be able to remove it as well.我的意思是,如果我手动启动的垃圾收集可以删除这个“thrash”,那么正常的GC执行也可以删除它。 Why isn't it happening?为什么没有发生?

I've even tried to call System.gc() when the stages are being switched, but, even though the garbage collection happens, this "thrash" memory isn't removed like when I manually perform the GC .我什至尝试在切换阶段时调用System.gc() ,但是,即使发生了垃圾收集,也不会像我手动执行GC那样删除这个“thrash”内存。 Am I missing something important about how the garbage collector works or about how Android implements it?我是否遗漏了一些关于垃圾收集器如何工作或 Android 如何实现它的重要信息?

Final considerations最后的考虑

I've spent days searching, studying and making modifications on my code but I could not find out why this is happening.我花了几天时间搜索、研究和修改我的代码,但我找不到为什么会这样。 StackOverflow is my last resort. StackOverflow 是我最后的选择。 Thank you!谢谢!

NOTE: I was going to post some possibly relevant part of my app's source code, but since the question is already too long I will stop here.注意:我打算发布一些可能与我的应用程序源代码相关的部分,但由于问题已经太长了,我将在这里停止。 If you feel the need to check some of the code, just let me know and I will edit this question.如果您觉得需要检查某些代码,请告诉我,我将编辑此问题。

What I have already read:我已经读过的:
How to force garbage collection in Java? 如何在 Java 中强制垃圾收集?
Garbage collector in Android Android中的垃圾收集器
Java Garbage Collection Basics by Oracle Oracle 的 Java 垃圾收集基础
Android Memory Overview Android 内存概览
Memory Leak Patterns in Android Android 中的内存泄漏模式
Avoiding Memory Leaks in Android 避免 Android 中的内存泄漏
Manage your app's memory管理您的应用程序的内存
What you need to know about Android app memory leaks 您需要了解的有关 Android 应用程序内存泄漏的信息
View the Java heap and memory allocations with Memory Profiler使用 Memory Profiler 查看 Java 堆和内存分配
LeakCanary (memory leak detection library for Android and Java) LeakCanary(Android 和 Java 内存泄漏检测库)
Android Memory Leak and Garbage Collection Android 内存泄漏和垃圾收集
Generic Android Garbage Collection 通用 Android 垃圾收集
How to clear dynamically created view from memory? 如何从内存中清除动态创建的视图?
How References Work in Android and Java 引用如何在 Android 和 Java 中工作
Java Garbage Collector - Not running normally at regular intervals Java 垃圾收集器 - 无法定期正常运行
Garbage Collection in android (Done manually) android中的垃圾收集(手动完成)
... and more I couldn't find again. ......还有更多我再也找不到了。

Garbage collection is complicated, and different platforms implement it differently.垃圾回收比较复杂,不同平台实现方式不同。 Indeed, different versions of the same platform implement garbage collection differently.事实上,同一平台的不同版本实现垃圾收集的方式不同。 (And more ... ) (和更多 ... )

A typical modern collector is based on the observation that most objects die young;一个典型的现代收藏家是基于对大多数物品年轻时死去的观察; ie they become unreachable soon after they are created.即它们在创建后很快就无法访问。 The heap is then divided into two or more "spaces";然后将堆分成两个或多个“空间”; eg a "young" space and an "old" space.例如“年轻”空间和“旧”空间。

  • The "young" space is where new objects are created, and it is collected frequently. “年轻”空间是创建新对象的地方,它经常被收集。 The "young" space tends to be smaller, and a "young" collection happens quickly. “年轻”的空间往往更小,“年轻”的收藏发生得很快。
  • The "old" space is where long-lived objects end up, and it is collected infrequently. “旧”空间是长期存在的对象结束的地方,它很少被收集。 On "old" space collection tends to be more expensive.在“旧”空间收藏往往更贵。 (For various reasons.) (出于各种原因。)
  • Object that survive a number of GC cycles in the "new" space get "tenured";在“新”空间中存活多次 GC 周期的对象将获得“终身使用权”; ie they are moved to the "old" space.即它们被移动到“旧”空间。
  • Occasionally we may find that we need to collect the new and old spaces at the same time.偶尔我们可能会发现需要同时收集新旧空间。 This is called a full collection.这称为完整集合。 A full GC is the most expensive, and typically "stops the world" for a relatively long time.完整的 GC 是最昂贵的,并且通常会在相对较长的时间内“停止世界”。

(There are all sorts of other clever and complex things ... which I won't go into.) (还有各种其他巧妙而复杂的东西……我不会深入讨论。)


Your question is why doesn't the space usage drop significantly until you call System.gc() .您的问题是为什么在您调用System.gc()之前空间使用量不会显着下降。

The answer is basically that this is the efficient way to do things.答案基本上是,这是做事的有效方式。

The real goal of collection is not to free as much memory all of the time.收集的真正目标不是一直释放尽可能多的内存。 Rather, the goal is to ensure that there is enough free memory when it is needed, and to do this either with minimum CPU overheads or a minimum of GC pauses.相反,目标是确保在需要时有足够的空闲内存,并以最小的 CPU 开销或最小的 GC 暂停来做到这一点。

So in normal operation, the GC will behave as above: do frequent "new" space collections and less frequent "old" space collections.因此,在正常操作中,GC 的行为将如上:频繁地进行“新”空间收集和不频繁地进行“旧”空间收集。 And the collections will run "as required".并且集合将“根据需要”运行。

But when you call System.gc() the JVM will typically try to get back as much memory as possible.但是当您调用System.gc() ,JVM通常会尝试取回尽可能多的内存。 That means it does a "full gc".这意味着它会执行“完整的 gc”。

Now I think you said it takes a couple of System.gc() calls to make a real difference, that could be related to use of finalize methods or Reference objects or similar.现在我想你说过需要几次System.gc()调用才能产生真正的不同,这可能与使用finalize方法或Reference对象或类似方法有关。 It turns out that finalizable objects and Reference are processed after the main GC has finished by a background thread.事实证明,在主 GC 完成后,后台线程会处理可终结对象和Reference The objects are only actually in a state where they can be collected and deleted after that.对象实际上只是处于可以收集和删除之后的状态 So another GC is needed to finally get rid of them.所以需要另一个 GC 来最终摆脱它们。

Finally, there is the issue of the overall heap size.最后,还有整体堆大小的问题。 Most VMs request memory from the host operating system when the heap is too small, but are reluctant to give it back.当堆太小时,大多数 VM 会从主机操作系统请求内存,但不愿意将其归还。 The Oracle collectors note the free space ratio at the end of successive "full" collections. Oracle 收集器会记录连续“完整”收集结束时的可用空间比率。 They only reduce the overall size of the heap if the free space ratio is "too high" after a number of GC cycles.如果在多次 GC 周期后可用空间比率“太高”,它们只会减小堆的整体大小。 There are a number of reasons that the Oracle GCs take this approach: Oracle GC 采用这种方法的原因有很多:

  1. Typical modern GCs work most efficiently when the ratio of garbage to non-garbage objects is high.当垃圾对象与非垃圾对象的比率很高时,典型的现代 GC 工作效率最高。 So keeping the heap large aids efficiency.因此,保持堆大有助于提高效率。

  2. There is a good chance that the application's memory requirement will grow again.应用程序的内存需求很有可能再次增长。 But the GC needs to run to detect that.但是 GC 需要运行才能检测到。

  3. A JVM repeatedly giving memory back to the OS and and re-requesting it is potentially disruptive for the OS virtual memory algorithms. JVM 反复将内存返还给操作系统并重新请求它,这可能会破坏操作系统虚拟内存算法。

  4. It is problematic if the OS is short of memory resources;如果操作系统缺少内存资源,就会出现问题; eg JVM: "I don't need this memory. Have it back", OS: "Thanks", JVM: "Oh ... I need it again!", OS: "Nope", JVM: "OOME".例如 JVM:“我不需要这个内存。把它拿回来”,操作系统:“谢谢”,JVM:“哦……我又需要它了!”,操作系统:“不”,JVM:“OOME”。

Assuming that the Android collector works the same way, that is another explanation for why you had to run System.gc() multiple times to get the heap size to shrink.假设 Android 收集器的工作方式相同,这就是为什么必须多次运行System.gc()才能缩小堆大小的另一种解释。


And before you start adding System.gc() calls to your code, read Why is it bad practice to call System.gc()?在开始向代码添加System.gc()调用之前,请阅读为什么调用 System.gc() 是不好的做法? . .

I got the same problem on my app, I seen you have understood the GC, try to watch this video on why the GC is needed.我在我的应用程序上遇到了同样的问题,我看到您已经了解 GC,请尝试观看此视频,了解为什么需要 GC。 try to add this code to your app class (the java file of the app, like each java file for each activity) and add this code under the Override of the "onCreate" (the code is in kotlin)尝试将此代码添加到您的应用程序类(应用程序的java文件,就像每个活动的每个java文件一样)并将此代码添加到“onCreate”的覆盖下(代码在kotlin中)

here is the hole class:这是洞类:

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
    }
}

this bit of code make that every 6 sec GC is called这段代码使得每 6 秒 GC 被调用一次

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM