小对象池比Android的Java垃圾收集器更高效吗?

5
所以,我在阅读这篇文章:http://www.ibm.com/developerworks/java/library/j-jtp09275/index.html时,看到了这句话:"公共服务公告:对象池现在对于除了最重量级的对象外都是严重的性能损失,即使针对这些对象,不引入并发瓶颈也很棘手。" 我原封不动地接受了这个说法。该文章讨论了分代 GC、释放、线程本地分配和逃逸分析。

然而,我脑海中突然有一个小声音问我:“但是这是否适用于 Android 中垃圾收集器的实现?”我不知道答案。我甚至不知道如何找到答案。

我记得在我的 Android 应用中为经常使用的小对象实现对象池后,垃圾收集器运行的次数较少。不确定是否意味着更快的应用程序... 此外,GC 在没有对象池的情况下更频繁地运行(根据 logcat),因此我认为 Android 的 GC 实现输给了对象池.. 但这个假设几乎没有依据,因为我没有注意到使用或不使用对象池会有显著的性能差异。

那么,这里是否有人知道对象池对于经常使用的小对象是否比 Android 的 GC 更有效?


值得注意的是,“现在”是来自于一篇2005年的文章。 - SJuan76
这并不是一个公平的比较,因为手持客户端软件可以对资源需求进行明确的限制,而应用服务器软件需要支持任意数量的线程/节点竞争池化资源。 - Affe
@SJuan76 真的。不过你对GC有什么经验吗?我只制作过一些小型休闲游戏,从未达到需要优化代码的程度,但我觉得了解是否应该使用池还是很重要的。 - Justin AnyhowStep
@Affe,我不太明白你的意思。虽然,如果我需要支持多个线程,我会为每个线程分配自己的池。我只是在询问Android的GC,而不是比较客户端和服务器资源需求。 - Justin AnyhowStep
查看Message类,这是谷歌认为值得池化的实现的一个很好的例子。https://developer.android.com/reference/android/os/Message.html https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/os/Message.java - Krylez
显示剩余4条评论
2个回答

5
这取决于如何衡量“高效”,“小”和“常用”。在Android内部的几个地方使用对象池,例如:适配器框架(Adapter framework)整个AdapterView(ListView和kin)都是围绕对象池设计的,这次是为相对较重的对象(例如,ListView行很容易达到几十KB)。SensorEvent对象是可回收的,这次是为了轻量级对象,每秒可能使用数十次。AttributeSet对象是可回收的,作为View膨胀的一部分等等。其中一些基于早期版本的Dalvik和Android,在那时我们的目标是在纯解释语言和相当天真的GC引擎下达到低于100MHz的CPU。然而,即使在今天,对象池也有一个超越即时性能的重大优势:堆碎片。Java的GC引擎是紧凑型垃圾收集器,这意味着连续的自由堆空间块被组合成更大的块。Dalvik的GC引擎是非压缩垃圾收集器,这意味着您分配的块永远不会成为更大块的一部分。这就是许多开发人员在位图管理中受挫的地方——他们得到的OutOfMemoryError并不是因为堆已满,而是因为堆没有足够大的块来进行所需的分配,这是由于堆碎片化。对象池通过防止池中的对象再次被垃圾回收并且不经常为池分配新对象(仅在池需要增长时,由于过多同时使用)来避免堆碎片化。游戏开发人员长期以来一直在Android中使用对象池,这源于Android的垃圾回收是非并发的,“停止世界”当GC进行时。现在,大多数Android设备使用并发垃圾收集器,在这里缓解了一些痛苦。因此,对象池绝对仍然是一种相关的技术。但是,我认为它主要是作为检测到问题后采取的反应(例如,Traceview显示GC中时间过长,Traceview显示对象构造函数中时间过长,MAT显示您有足够的堆但您会得到OutOfMemoryErrors)。例外是游戏开发——游戏开发人员可能具有使用现代Android设备仍需要池化的启发式算法。

非常棒的答案,谢谢!那么我可以认为池化对于较大的对象更重要吗?因为较大的对象需要更多的连续内存,将它们池化在开头,然后让小对象在各个地方分配和释放应该是可以的,对吧?我的意思是,所有的分配和释放都会使堆碎片化,但如果分配的都是小对象,这种碎片化必须非常严重才能阻止甚至小对象的分配(我希望这个逻辑是正确的)。 - Justin AnyhowStep
@JustinAnyhowStep 对象会被复制并压缩以减少碎片化。如果你想避免对垃圾回收器的压力和减少垃圾回收次数,那么对象池更有用。但如果这不是问题,它们可能会给你的代码增加不必要的复杂性。如果不确定,我建议选择你认为最简单的选项。 - Peter Lawrey
“对象池避免堆碎片……” - 这取决于您如何填充对象池。例如,如果池对象是惰性创建的,或者稍后创建的非池对象被半永久地“附加”到池中的对象上,则可能再次出现碎片化。真正的解决碎片化问题的唯一方法是Android实现压缩垃圾收集器。 - Stephen C
@StephenC:“唯一真正的消除碎片化的方法是让Android实现紧凑式垃圾收集器”——毫无疑问。 - CommonsWare
@JustinAnyhowStep:“因为更大的对象需要更多的连续内存,所以将它们汇聚在开始,然后让较小的对象在各个地方分配和释放应该没问题,对吗?”-- 更大的分配需要更多连续空间。一个“对象”通常由对其他对象的引用组成。因此,ListView中的一行很昂贵,因为所有小部件和容器的大小之和很大,但这里的池化更多是为了CPU(例如,在滚动时避免抖动)。对于堆碎片,最重要的是重复使用位图,因为位图是大块。 - CommonsWare

2
你的推理有一个谬误。GC更频繁地运行并不意味着某种性能下降。那些更频繁的GC运行可能比那些要混过对象池的不太频繁的GC运行更快且时间更短。
话虽如此,我做了一些研究,以下是我的一些想法......几年前,我的手机只有一个核心。运行GC意味着从活动切换到GC线程。即使具有并发GC和多个内核(现代设备大约有2-5个内核),也可能会有轻微的暂停。
为游戏预分配用户在下一个交互序列中可能需要的所有内容是一个好主意。基本上遵循实时应用程序的口号,相对于总体性能,它们更少关注应用程序的用户体验部分具有一致可测量的性能。 http://developer.android.com/training/articles/perf-tips.html

我从未想过这个角度,但我希望我能记得查看logcat所提供的那些“ms”值。 - Justin AnyhowStep
不,不,不是讽刺。我真的没有那么想过 =/ 我一直被告知GC等于糟糕,所以我没有想到要仔细检查它。不,我没有日志;我从来没有保存过它们。 - Justin AnyhowStep
谢谢帮忙!我想我会保留我的代码,直到遇到问题为止。我很担心,因为我在我的代码库中的数学部分都使用 Vector2 类,稍后更改会比现在处理问题需要更多时间。上次我尝试使用 Vector2 对象池时,代码几乎无法阅读,因为我必须在使用向量数学(经常使用)之前始终预先分配 Vector2 实例,并且之后必须对其进行解除分配。这添加了很多没有“意义”的代码,混淆了代码的真正意图(依我之见)。 - Justin AnyhowStep

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