Android 垃圾回收内存碎片化失败。解决方法?

19

我正在测试安卓3.1系统,使用了大堆内存选项,大约有250M的可用内存。

我设置了以下代码,每当我在应用程序设置中点击“测试”按钮时运行:

float [][][]foo = new float[3][2048][2048];
Bitmap bm = Bitmap.createBitmap(2048, 2048, Bitmap.Config.ARGB_8888);
bm.recycle();
bm  = null;
foo = null;
我有足够的内存可以多次点击按钮而没有问题。但是,如果我继续点击按钮,最终(少于20次点击)会出现OutOfMemory错误。[通常在android.graphics.Bitmap.nativeCreate(本机方法)中]除了我点击按钮时显示的一个小Toast之外,没有其他活动。这是由于内存碎片还是android位图代码和/或GC中的可怕错误?还是我做了一些愚蠢的事情?(请让它变得愚蠢...)是否有人有解决方法?因为上述情况相当代表了每次用户调用该代码时必须执行的操作,而且现在即使变量清除非常谨慎,也会在几次使用后死亡。(这已经让我困扰很长时间了!)
[更新]
我确认这是一个内存碎片问题或gc bug,因为堆转储显示我空闲时仅使用5.6M(无泄漏),处理期间峰值约为26M。(此外,本机堆仍保持在4M以下。)同时,java堆的范围不断扩大到测试设备的280M限制,在此点处开始收到OutOfMemory异常。因此,我只在峰值时使用了可用堆的10%,但却出现了OutOfMemory。
[添加对System.gc()的调用不幸地修复了我给出的简单测试用例。我说不幸是因为(A)它不应该有任何影响,(B)因为我已经定期在我的真实代码中调用它,所以这意味着我的简单测试用例太简单了。]
有其他人遇到过这种情况吗?有什么解决方法?是否有一种优雅的方式来重新启动我的应用程序?
[更新]
以下版本可靠地导致OutOfMemory出现3至4次调用(按钮按下):
float [][][]foo = new float[3][2048][2048];
Bitmap bm = Bitmap.createBitmap(2048, 2048, Bitmap.Config.ARGB_8888);
int []bar = new int[3*2048*2048];
bm.recycle();
bm = null;
System.gc();
foo = null;
System.gc();
bar = null;
System.gc();

内存跟踪显示堆在每次调用时稳定增长,直到达到极限并死亡。如果我删除其中任何一个分配,它就会达到平衡并无限存活。只删除除最后一个gc()之外的所有操作会导致它稍微更快地死亡。

我认为这是一个碎片化问题,不是 gc 的 bug 本身。如果有人知道如何解决它,请告诉我。int[] 分配用于编写位图,因此我没有将其分配为二维数组的选项(这是 Android Bitmap 库的限制)。


我不知道这是否有帮助,但Dalvik使用标记-清除类型的GC。它会尝试等待直到所有可用内存都被使用,然后一次性清理它(性能问题,因为它需要停止处理以进行GC)。也许System.gc();调用正是解决此问题所需的,因为GC期望较小的对象。 - Ancantus
1
@Ancantus - 不幸的是,就像我之前提到的,在我的实际代码中,我在每次recycle()/null之后都会调用System.gc(),但仍然出现这种情况。我觉得这可能是一个碎片化问题或者System.gc()在后台线程中被忽略或表现不同的原因。如果有可靠的方法,我愿意杀死并从头重新启动我的应用程序以压缩内存。我也想要一个调用来告诉我,在GC后,Java堆中有多少空闲空间(这将立即且确定地告诉我是否存在碎片化问题或者模糊泄漏)。是否有这样的方法呢? - Brandyn
1
@Ancantus - 顺便说一下,我在我的主内存分配对象中添加了一个带有调试消息的终结器,并且当我调用System.gc()时它实际上被终止了。 实际上,在没有泄漏的情况下,我的所有gc似乎都完美地工作,并且甚至在处理过程中的关键点清理事物以将峰值使用量最小化。 但是,所有这些只对MAT等堆调试工具可见。 与此同时,Android设备每次增加20-50M的Java堆,直到死机。 究竟怎么回事?? - Brandyn
浮点数数组是否必须触发崩溃?也许这是多维数组的问题?如果您将其设置为具有32k2k元素的一维数组呢? - Stefan Haustein
1
你尝试过使用float []foo = new float[320482048]吗?我倾向于认为这是某种碎片化问题。是否有最大图像大小限制,即您可以为最大大小分配数组和位图,然后始终保留它们而不是重新分配? - Stefan Haustein
显示剩余10条评论
3个回答

4

这里有一个看起来可以解决此问题的 SO 页面:

Strange out of memory issue while loading an image to a Bitmap object

具体而言,Ephraim 的回答(摘录):

"1) 每次使用 BitmapFactory.decodeXYZ() 时,确保传入一个设置为 inPurgeable(最好也设置为 inInputShareable)的 BitmapFactory.Options。

"2) 永远不要使用 Bitmap.createBitmap(width, height, Config.ARGB_8888)。我的意思是永远不要!我从未遇到过这个方法在几次尝试后没有引发内存错误的情况。无论 recycle(), System.gc() 还是其他方法,都无济于事。它总是会引发异常。另一种实际可行的方法是在你的可绘制资源中添加一个虚拟图像(或者通过上述第一条步骤解码的另一个 Bitmap),将其按比例缩放至所需大小,然后操作生成的 Bitmap(例如将其传递给 Canvas 进行更多操作)。因此,你应该使用以下方法:Bitmap.createScaledBitmap(srcBitmap, width, height, false)。如果出于某种原因你必须使用 brute force create 方法,则至少要传递 Config.ARGB_4444。

在评论中,有些人说这解决了他们的问题,而这与此处 OP 的问题非常相似。

我想补充一点,Diane Hackborn 曾经评论过,在 3.0 Android 中不再从本地堆中分配位图,而是直接从常规堆中分配。这可能使你的本地堆数据失去意义。请参见 hackbod 在此页面上的评论:

Bitmaps in Android

我猜这意味着在 Honeycomb 上关于位图分配有一个相当重大的变化,因此这可能解释了为什么会出现这样的分配错误(如果出现)。我不知道此更改对 recycle() 命令产生了什么影响,但鉴于 Ephraim 的上述评论,答案可能是“不太好”。

最后:

使用 largeHeap 来加载巨大的位图可能被认为是与其他应用程序不友好的行为,特别是如果你接近设备的物理极限。我不确定如何避免这种情况,但要准备好频繁调用 onPause() / onResume() 方法,因为你的应用程序会占用其他应用程序的资源,而它们也会占用你的资源。此 SO 回答包括了这方面的讨论:

Detect application heap size in Android


"inPurgeable"现已被弃用并被忽略。 - zyamys

1
为了避免碎片化,您可以一次性分配大数组和位图,并重复使用它们。
对于Android,这有一些注意事项,因为Android会在一定程度上管理您的应用程序资源。例如,如果Activity或View不可见,则可能会被卸载,并在稍后重新运行,如果它们再次变得可见。因此,较大的东西最好由Application对象或静态位置存储。
如果仅用于某些首选项对话框,则应在第一次使用时保留它,但在此后保留它,以避免在每次运行时使用太多内存。如果很少使用,则可能应在离开首选项屏幕后重新启动应用程序。如果做得好,用户不需要注意到它,您将获得一个新鲜且内存友好的进程。

很遗憾,这是一个图像处理应用程序,它接受用户提供的各种大小的图像(没有硬性最大限制——最大限制取决于可用内存容量)。 - Brandyn
即使没有静态数组和Bitnaos,您仍然可以尝试在一个或多个图像后重新启动应用程序。也许只需通过finish()退出最后一个“Activity”,然后通过startActivity重新打开它就足够了。但是VM可能会以“智能”的方式重复使用,因此可能需要在退出时运行一个辅助服务,以再次启动您的主要“Activity”。 - dronus
@Brandyn 2016年,内存碎片问题仍然存在。我最初使用LruCache,但在使用了几个月后,发现它会因为内存碎片而导致OutOfMemory的错误。请参见 https://github.com/andstatus/andstatus/issues/351 目前我想尝试的唯一解决方案是一次性分配图像缓存,然后重复使用缓存的位图内存。我将不得不分配最大尺寸的位图,以便可以重复使用... - yvolk

-2

他正在使用3.1版本,其中Bitmap.recycle()不再需要直接调用。 - David Liu

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