当重新启动一个活动时,为什么堆内存会增加?

13

这个问题涉及到Android中的内存。

我的方法:

我有两个活动,A和B。从A中启动B的方法如下:

Intent i = new Intent(A.this, B.class);
startActivity(i);

在B中点击按钮时,我会执行以下操作:

B.this.finish();
  • 我在B中重写onDestroy方法,并将所有引用设为null。
  • 在A的onResume方法中,我没有分配新的内存。
  • 我没有泄漏上下文。
  • 我没有使用多个线程。
  • 我没有使用服务。
  • B中的所有变量都是私有类变量,并且它们在B的onDestroy中全部设置为null。
  • B中的ImageView对象在B的onDestroy中背景被设置为null。
  • 我确定B被销毁了。

结果:

当我在Activity A中时,堆内存为7.44 MB。然后当我启动B并在B上调用finish(从而返回到A),堆增加了0.16 MB。再次重复此过程,每次堆都会增加0.08 MB。

  • 我没有查看堆限制,而是查看已分配的堆。
  • 我在B的onDestroy方法的末尾调用System.gc()。

附加信息:

-我使用MAT分析内存分配并尝试找到此泄漏。奇怪的是,Activity B似乎有5个实例。正如它发生的那样,我重复了startActivity/finish过程5次。最底部的条目是Activity,其他条目是活动中的监听器:

enter image description here

这是dominator树的截图。我找不到任何异常或可疑之处。

Dominator Tree

-我看过关于内存使用(和泄漏)的两个谷歌IO视频。

问题:

无论我做什么,都有可能分配这0.08 MB的堆(并且无法被GC收集)吗?如果不行,有什么可能会导致这种情况?

更新:

  1. 我尝试在B中启动Activity而不设置内容视图。这意味着B是一个完全空的活动。结果是,当我多次重新启动活动时,堆内存没有增加。但请注意,这不是解决方案。我必须能够设置内容视图。

  2. scorpiodawg:我尝试在模拟器上运行我的应用程序,但堆仍然增长。很好的尝试。

  3. ntc:我在所有可能的地方将所有出现的“this”更改为“getApplicationContext()”。因为setContentView需要引用布局文件而不是上下文,所以我无法调用setContentView(getApplicationContext())。我取而代之的是在Activity B的onDestroy方法中创建一个空布局文件,并调用setContentView(emptylayout)。但这没有帮助。

  4. 我尝试删除所有代码,只调用setContentView(mylayout)。问题仍然存在。然后我从布局XML文件中删除了所有GUI元素。问题仍然存在。唯一剩下的是容器视图,几个嵌套的线性、相对和滚动布局。我尝试删除滚动条中的“android:scrollbarDefaultDelayBeforeFade”属性设置。结果很好,内存泄漏消失了。然后我重新放回了之前删除的所有代码,但没有设置“android:scrollbarDefaultDelayBeforeFade”属性,内存泄漏又出现了。多么奇怪啊!


但是,如果您一遍又一遍地重复这个过程,您会得到OutOfMemory异常吗? - Jett Hsieh
是的,最终我会的。 - Emir Kuljanin
在MAT截图中,我只能看到Listener。你是否在onDestroy方法中重置它们?mView.setOnClickListener(null); - ol_v_er
我已经尝试过了,但没有成功。而且不仅仅是监听器,最后一个条目是活动。 - Emir Kuljanin
你会还是正在做?你真的看到了OutOfMemoryError吗? - njzk2
显示剩余5条评论
5个回答

11

如果您有5个B活动的实例,则您没有正确管理活动堆栈。 我发现用CLI命令进行检查是最好的方法:

adb shell dumpsys meminfo 'your apps package name'

当我在一个含两个活动的项目中切换时,我遇到了类似的问题。每次我切换时,上述命令显示我的堆栈上都有一个新的实例。然后,我使用FLAG_ACTIVITY_REORDER_TO_FRONT标志为启动的活动设置代码,例如:

Intent i = new Intent("com.you.yourActivityB");
i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(i);

我完成这个步骤后,使用adb shell命令在我的两个活动之间切换时不再显示更多的实例。


在两个活动中设置i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);解决了问题!在这个问题上花费了大约30个小时后,我无法感谢你们的帮助! - Emir Kuljanin

3

看起来你的活动中存在内存泄漏。

可能是因为你泄漏了上下文(在这种情况下是活动)。因此,请确保在活动的onDestroy方法调用时清除所有对上下文的引用。 更多细节请看这里

还要注意,在结束活动时可能存在未注销的内容观察者。


引用自己的话:“在B中,我重写了onDestroy方法并将所有引用设置为null。”和“我没有泄漏上下文。” - Emir Kuljanin
在MAT中似乎Activity正在泄漏。我猜你已经使用MAT跟踪了GC根路径。此外,监听器是否有任何对Activity或Activity成员的引用?正如你所说,有很多关于内存泄漏的文档和视频等,但对我来说,这个参考总是有帮助的: http://grubber.blog.hexun.com/58466974_d.html干杯。 - Francisco Jordano
我已经按照GC根路径进行了跟踪,但没有发现任何可疑的东西。虽然我必须承认,我不太确定要寻找什么。至少我找不到任何属于我的变量。至于MAT截图,你认为有泄漏吗?至于监听器,我尝试在B中完全不使用监听器进行编译,但堆增长仍然保持不变。这与我的监听器无关。 - Emir Kuljanin

2
我认为这是典型的Java问题。杀死一个Activity并不意味着它关联的对象应该从堆中移除,即使它们在失落区域(它们的引用为空)。因为这完全依赖于虚拟机何时调用垃圾收集器(无论你是否调用System.GC())。所以在条件接近内存不足时,它会调用并清理它(不能确定,可能是在它们的引用变为null后立即进行),所以我认为你不应该担心这个问题。
编辑: 调用setContentView(getApplicationContext)。
无论你在哪里使用this关键字传递上下文,请将其更改为this.getApplicationContext()。

我应该担心这个问题,因为当我重启Activity B很多次时,0.08 MB最终堆积超过了最大堆限制,导致OOM异常。当内存几乎耗尽时,GC并没有及时介入来拯救我的应用程序。 - Emir Kuljanin

1

考虑使用

android:launchMode="singleTask"

在你的AndroidManifest.xml文件中。点击这里


0
这个0.08 MB的堆空间是否可能会一直被分配(且无法被GC回收),无论我做什么?如果不是,那么有没有任何想法是什么原因导致的?
如果未使用,则系统会在认为需要时收回0.08MB的堆空间。垃圾回收并不会在调用System.gc()后立即发生,它更像是垃圾回收请求,在可能的情况下尽快进行。一个对象从分配到从内存中删除通常需要很长时间。Java虚拟机规范和Java语言规范指定了正在经历以下阶段的对象的生命周期:
1. 创建 2. 使用中(强可达) 3. 不可见 4. 不可访问 5. 已回收 6. 终结 7. 释放
请注意,当对象变得不可访问时,它仅成为垃圾回收的候选对象,具体取决于何时进行垃圾回收操作。
即使是像Dalvik这样的小型JVM,我也不认为对象会在如此短的时间内经历完整个生命周期。由于垃圾回收是一项昂贵的操作,所以只有在需要时才会执行。
如果您有需要快速从内存中删除的对象(出于某种原因),那么请尝试使用WeakReference。或者,您可以提供更多上下文信息,以便其他人能够更好地帮助您。
有关Java中GC发生方式的更多信息,请参见这是一篇非常好的文章

谢谢提供的链接,内容很有用。然而,这让我得出结论,0.08 MB 的内存从未达到“不可访问”的状态。这令人担忧,因为我在 Activity B 的 onDestroy 方法中将所有引用设置为 null。所以基本上,我不知道是什么分配了 0.08 MB 的内存。 - Emir Kuljanin
将引用设置为null并不会摆脱引用曾经持有的对象。那0.08 MB被很多东西所持有,从创建活动的意图到活动的上下文,还有很多其他的东西。是你只是在折腾,试图理解一些东西,还是你有一些目标,但不知道如何实现?我不是说内存管理不是你关心的问题,但在大多数情况下,你永远不必过多担心它。只要打开Logcat并查看垃圾回收发生的频率即可。 - Archit
你所关心的0.08 MB,可能已经达到了无法访问的状态。然而,在对象被从内存中移除之前,它们至少会经历两个更多的状态。另外,值得一提的是,即使在最终化阶段,对象也有可能再次变得可访问。 - Archit
一遍又一遍地启动和重新启动Activity B并不是一个好主意。如果必须这样做,可以尝试使用singleInstance启动模式,并确保指示您将自己处理方向和键盘更改。这应该(不完全确定)确保只创建一个活动实例。 - Archit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接