垃圾收集器未释放“垃圾邮件"就像在Android应用程序中一样 [英] Garbage Collector not freeing "trash memory" as it should in an Android application

查看:84
本文介绍了垃圾收集器未释放“垃圾邮件"就像在Android应用程序中一样的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

你好!

我是Java和Android的初学者,最近在处理应用程序的内存管理时遇到了麻烦.我将这些文本分成几部分,以使其更清晰易读.

我的应用的简要说明

这是一个由几个阶段(关卡)组成的游戏.每个阶段都有玩家的起点和出口,出口将玩家带到下一个阶段.每个阶段都有其自己的障碍.当前,当玩家到达最后阶段(到目前为止,我只创建了4个)时,他/她会自动返回到第一阶段(第1级).

名为 GameObject 的抽象类(扩展了 Android.View )定义了玩家以及游戏中存在的所有其他对象(障碍物)的基本结构和行为.所有对象(本质上是视图)都在由我创建的自定义视图中绘制(扩展了FrameLayout).游戏逻辑和游戏循环由侧线程(gameThread)处理.这些阶段是通过从xml文件中检索元数据创建的.

问题

除了我的代码上所有可能的内存泄漏(我一直在努力寻找和解决所有这些问题)外,还有一个与垃圾收集器发生有关的奇怪现象.我将使用图像而不是用文字描述它并冒着使您困惑的风险.正如孔子所说:一个图像值得一千个单词".好吧,在这种情况下,由于我下面的GIF包含150帧,因此我已经避免了阅读15万个单词的麻烦.

说明:第一张图片代表首次加载"stage 1"时我的应用程序的内存使用情况.第二张图片(GIF)首先代表我的应用程序的内存使用时间线,这是第二次加载阶段1"时(如前所述,当玩家击败最后一个阶段时,会发生这种情况),其后是四个垃圾我强行发起的收藏.

您可能已经注意到,两种情况之间的内存使用量存在巨大差异(将近50MB).第一次加载阶段1"时,在游戏开始时,该应用程序正在使用85MB的内存.当第二次加载同一阶段时,稍后,内存使用量已经达到130MB!那可能是由于我的编码不好,因此我不在这里.在我强制执行2个(实际上是4个,但只有前2个很重要)垃圾回收之后,您是否注意到内存使用率又回到了正常状态"(与首次加载该阶段时相同的内存使用率)? 那是我正在谈论的怪异现象.

问题

如果应该将垃圾收集器从内存中删除不再被引用的对象(或者至少仅具有弱引用),则为什么是仅当我强行调用 GC 而不是在 GC的正常执行时,您上面看到的垃圾内存"才被删除?我的意思是,如果由我手动启动的垃圾收集可以删除此崩溃",然后正常的 GC的执行也可以删除它.为什么不发生呢?

我什至尝试在切换阶段时调用 System.gc(),但是,即使发生了垃圾回收,这种崩溃"的内存也不会就像我手动执行 GC 一样删除了.我是否缺少有关垃圾收集器的工作方式或有关Android实现方式的重要信息?

最终注意事项

我花了几天的时间搜索,研究和修改我的代码,但我无法找出为什么会发生这种情况. StackOverflow是我的不得已的方法.谢谢!

注意::我打算在应用程序源代码中发布一些可能相关的部分,但是由于问题已经太久了,因此我将在此处停止.如果您需要检查一些代码,请告诉我,我将编辑此问题.

我已经阅读的内容:
如何在Java中强制垃圾收集?
Android中的垃圾收集器
Oracle的Java垃圾收集基础
Android内存概述
Android中的内存泄漏模式
避免Android中的内存泄漏
管理应用的内存
您需要了解的有关Android应用内存泄漏的信息
使用Memory Profiler查看Java堆和内存分配
LeakCanary(适用于Android和Java的内存泄漏检测库)
Android内存泄漏和垃圾收集
通用Android垃圾收集
如何从内存中清除动态创建的视图?
引用如何在Android中工作和Java
Java垃圾收集器-无法定期正常运行
Android中的垃圾收集(手动完成)
...还有更多我找不到的东西.

解决方案

垃圾收集很复杂,不同的平台实施方式也不同.实际上,同一平台的不同 version 实施垃圾收集的方式有所不同. (还有更多……)

典型的现代收藏家所基于的观察结果是,大多数物体早逝.也就是说,它们在创建后很快就无法访问.然后将堆划分为两个或更多的空间".例如一个年轻"的空间和旧"的空间空间.

  • 年轻人"空间是创建新对象的地方,并且经常收集它. 年轻"的空间趋于变小,并且年轻"的空间趋于小.收集很快.
  • 老"字样空间是长期存在的对象的结局,并且很少收集.在旧的"空间收集往往更昂贵. (出于各种原因.)
  • 在新"数据库中存活多个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. 如果OS缺少内存资源,这是有问题的.例如JVM:我不需要此内存.将其恢复",OS:谢谢",JVM:哦……我再次需要它!",OS:否",JVM:"OOME".

假设Android收集器的工作方式相同,这是为什么必须多次运行System.gc()来缩小堆大小的另一种解释.


在开始向代码中添加System.gc()调用之前,请阅读

Description: the first image represents my app's memory usage when the "stage 1" is first loaded. 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.

As you might have noticed, there is a huge difference (almost 50MB) in the memory usage between the two situations. When the "Stage 1" is firstly loaded, when the game starts, the app is using 85MB of memory. When the same stage is loaded for the second time, a little bit later, the memory usage is already at 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)? 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? 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. 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. Am I missing something important about how the garbage collector works or about how Android implements it?

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. 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?
Garbage collector in Android
Java Garbage Collection Basics by Oracle
Android Memory Overview
Memory Leak Patterns in Android
Avoiding Memory Leaks in Android
Manage your app's memory
What you need to know about Android app memory leaks
View the Java heap and memory allocations with Memory Profiler
LeakCanary (memory leak detection library for Android and Java)
Android Memory Leak and Garbage Collection
Generic Android Garbage Collection
How to clear dynamically created view from memory?
How References Work in Android and Java
Java Garbage Collector - Not running normally at regular intervals
Garbage Collection in android (Done manually)
... 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; i.e. they become unreachable soon after they are created. The heap is then divided into two or more "spaces"; e.g. 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"; i.e 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.

(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().

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.

So in normal operation, the GC will behave as above: do frequent "new" space collections and less frequent "old" space collections. 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. That means it does a "full 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. It turns out that finalizable objects and Reference are processed after the main GC has finished by a background thread. 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.

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. The Oracle collectors note the free space ratio at the end of successive "full" collections. They only reduce the overall size of the heap if the free space ratio is "too high" after a number of GC cycles. There are a number of reasons that the Oracle GCs take this approach:

  1. Typical modern GCs work most efficiently when the ratio of garbage to non-garbage objects is high. 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.

  3. A JVM repeatedly giving memory back to the OS and and re-requesting it is potentially disruptive for the OS virtual memory algorithms.

  4. It is problematic if the OS is short of memory resources; e.g. JVM: "I don't need this memory. Have it back", OS: "Thanks", JVM: "Oh ... I need it again!", OS: "Nope", 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.


And before you start adding System.gc() calls to your code, read Why is it bad practice to call System.gc()?.

这篇关于垃圾收集器未释放“垃圾邮件"就像在Android应用程序中一样的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆