如何写出GC友好的应用

作者:乐蛙科技高级研发经理 易宁

Android应用普遍没有iOS应用流畅,这与Android应用是运行在Java虚拟机上有很大的关系,而关系最大的恐怕就是Java虚拟机上的Garbage Collection(GC)了。Java语言提供了自动内存管理,垃圾对象会在GC过程中自动被释放。这提供了开发效率,使开发者能集中精力在业务逻辑上;但天下没有免费的午餐,你写的Android应用的某些性能上的问题很可能就是GC导致的。

下面是一段非常简单的Java语句:

1
byte[] data = new byte[512 * 512 * 4];

这条语句申请了一块大小相等于512*512大小图片的内存。执行这条语句会消耗多少时间呢?0+ms吗?基本没有性能影响吗?

实际的情况是:0200ms,甚至更多!这取决于应用当前Heap使用情况。在一个类似Hello World的测试程序里,执行这条语句有时候只需要0+ms就完成了,但一般情况是16+ms,甚至有时候需要30+ms。我们知道一段流畅的动画每帧绘图消耗的时间应该小于16ms,因为这样才能保证动画有60FPS的帧率。但如果执行上面那句简单的语句就要错过12帧,那就难怪动画不流畅了。

不过,有人可能要说了:“没人会傻到在每帧绘图的时候去申请一块这么大的内存,实际情况不会有这个问题”。实际的工程中的确不会这么写,但造成执行这条语句耗时比较长的原因不是因为申请的内存太大,而是申请过程中产生了GC,就算是一个Byte的内存申请也可能消耗比这更长的时间。

Dalvik中GC的原理

在Dalvik虚拟机中定义了四种触发GC的条件(参看Heap.h):

  • GC_CONCURRENT,当Heap的使用率达到某一阈值时自动触发。
  • GC_FOR_MALLOC,当Heap没有足够的空间用于容下新创建的对象时。
  • GC_EXPLICIT,用户主动调用GC时。
  • GC_BEFORE_OOM,当要发生OOM时系统尝试进行最后GC的努力。

GC_CONCURRENTGC_EXPLICIT是并行的,GC过程中应用不会被暂停,只在GC开始和结束时会暂停应用,每次暂停的时间比较短,一般只有3~4ms。GC_FOR_MALLOC是非并行的,GC过程中应用被暂停,耗时比较长,可能几十毫秒也可能几百毫秒。在应用向Dalvik申请内存时,Dalvik先检查当前Heap中有无足够的空余空间用来安排对象,当发现没有足够的空间的时候会先进行GC_FOR_MALLOC以试图释放垃圾对象来获取新的空间,如果发现空间还不够则进行Heap的增长。在每次成功分配完新的空间后,Dalvik会检查当前Heap的使用情况,如果使用空间超过一定的阈值的时候,GC_CONCURRENT就会触发。

每次GC的时候会打印如下的Log信息:

1
D/dalvikvm(10497): GC_CONCURRENT freed 2940K, 54% free 2885K/6204K, paused 1ms+4ms, total 38ms

上面的Log信息表明:这次GC_CONCURRENT释放了2940K的空间,当前Heap使用率为54%,总的Heap大小是6204K,空余2885K,GC开始时暂停了1ms,GC结束时暂停了4ms,GC总共花了38ms。

Dalvik虚拟机通过下面几个个属性值来控制Heap的空间配置:

  • targetUtilization,理想的Heap利用率,每当Heap增长时Dalvik会使增长后的Heap维持在这个利用率附近。
  • minFree,空余空间最小值。
  • maxFree,空余空间最大值。
  • startSize,虚拟机启动时初始的Heap大小。
  • growthLimit,用户设置的允许的最大Heap大小。
  • maximumSize, Heap空间最大极限值。

这些属性值都可以通过system/build.prop来配置,zygote启动dalvik虚拟机时会从该文件中读取这些参数。通常来说,Heap空余空间越大应用越流畅,消耗的内存也更多。

下图为一次“顺利”的4M内存申请:
Alt text
可以看到在申请之前,空余的空间达到8M,申请4M的内存很顺利没有任何GC发生。

下图为一次“不顺利”的8M内存申请:
Alt text
虽然空余的空间已经有8M,但是为了保证正常的Heap利用率,Heap空间还是增长了,并且增长Heap之前进行了一次GC_FOR_MALLOC

下图为继续申请8M的内存:
Alt text
相应的输出Log:

1
D/dalvikvm﹕ GC_FOR_ALLOC freed 8195K, 34% free 16726K/24972K, paused 23ms, total 23ms

上图和Log更加清晰的表明Heap空间不够时会先进行一次GC,GC的类型就是GC_FOR_MALLOC

Android流畅的关键:GC_FOR_MALLOC

GC_CONCURRENTGC_FOR_MALLOC虽然都是系统自动调用的,都暂停了应用,但它们花费的时间不在一个数量级。通过上面的分析我们知道每次申请内存空间不够时就会产生GC_FOR_MALLOC,我们不可能不申请内存,所以也不可能完全避免GC_FOR_MALLOC,但还是有些策略能降低GC_FOR_MALLOC的影响:

  1. 避免不必要的GC_FOR_MALLOC

    • Heap空间都有其理想的利用率,在理想的利用空间内,申请内存是不会发生GC_FOR_MALLOC
    • 应用应该避免内存开销的波动,将内存的波动维持在maxFreemixFree之间。
    • 避免大的内存需求。比如,不要轻易去用WallpaperManager,因为壁纸占用巨大的内存。
    • 应该尽量复用对象。比如,使用BitmapFactory加载图片时使用BitmapFactory.Option.inBitmap复用Bitmap,避免申请内存。
    • 对于已知有垃圾对象的情况下,先进行手动的System.gc()来释放空间,而不是等到系统自动调用GC_FOR_MALLOC。因为GC_EXPLICIT是异步的,暂停应用的时间远小于GC_FOR_MALLOC
    • 不要在循环中申请创建对象。循环中申请的对象会不断累积,直到空间不够发生GC_FOR_MALLOC
  2. 减少GC_FOR_MALLOC的影响

    • 不要在Android应用运行的关键阶段申请内存。比如,不要在onDraw, onLayout, onXXX中创建对象。在关键阶段创建对象可能使应用出现随机性卡顿。
    • 尽量在onCreateonStart阶段创建对象,同时在onStoponDestroy阶段释放对象。
    • 尽量复用ListView中Item,在ListView滑动时不要创建对象。
  3. 降低每次GC_FOR_MALLOC的时间消耗

    • 避免大量的,细小的对象。这些对象会增加Heap空间的复杂度,在GC时会严重影响GC的耗时。
    • 对于不用的对象近早将其引用消除,减少Heap空间占用和复杂度。
    • 尽量用数据结构,数组来组织对象。
    • 明确对象之间的关系,使对象之间的依赖关系简单明了。
    • 减少View的数量。每个View包含大量属性,对于没有交互的View,大多数的属性都是没有用的,可以用Drawable替代。

需要注意的是:本文讨论的只限于Dalvik虚拟机。对于Art虚拟机,官方文档已经表明Art虚拟机有极大的改进,其中就特别提到不用主动调用System.gc()来避免产生GC_FOR_MALLOC

转载请标明出处病已blog https://ivonhoe.github.io/