今天,咱们来学习下 Android 中的垃圾回收机制。git
你们应该知道,JVM 和 Dalvik 的垃圾回收机制实际并不彻底相同。而垃圾回收机制一直都是工做和面试中的必备技能,对 GC 有深刻的理解,才能在代码层面更好地去减小 GC 的发生,毕竟每次 GC 都会对主线程的运行形成必定的卡顿,从而影响到用户体验。github
本节将介绍支付宝 Android 客户端启动速度优化下的「垃圾回收」具体思路。面试
应用启动时间是移动 App 一个重要的用户体验环节,相对于普通的移动 App,支付宝过于庞大,必然会影响启动速度,一些常规的优化手段在支付宝中已经作得比较完善了,本篇文章尝试从 GC 的层面来进一步优化支付宝的启动速度。性能优化
相对于 C 语言来讲,Java 语言有一些特性,例如开发人员不用考虑内存的分配和回收,然而,进程内存管理又是必不可少的环节,妥协的结果是 Java 语言的设计者们把对象分配和回收放到了 Java虚拟机,这里但愿明确一个概念:GC 是有代价的,这个代价包括:阻塞 Java 程序的执行,占用 CPU 资源,占用额外内存等,谷歌的工程师意识到了 GC 对应用的影响,因此把 GC 的日志默认输出到了 Logcat,咱们常常可以看到 Logcat 里输出如下几种 GC 日志:架构
1.GC_EXPLICIT: Dalivk 给开发人员提供的主动触发 GC 的 API,读者能够参看 Google Maps 的设计来体会这个 API 的用法框架
2.GC_FOR _ALLOCK: 是分配对象失败时触发的 GC,这个 GC 会将应用全部的 Java 线程暂停运行,直到 GC 结束。ide
3.GC_CONCURRENT: 是 Java 虚拟机根据堆的当前状态触发的 GC,这个 GC 在 Dalvik 单独 GC 线程里运行,在部分时间里不影响应用 Java 线程的运行。 支付宝启动是一个典型的关键路径场景,咱们但愿看到尽量少的 GC_ CONCURRENT(若是可能, GC_ FOR_ ALLOCK 也应该缩减到最少),然而,经过 Logcat 咱们会看到很是糟糕的 GC 行为—大量的 GC_ FOR_ ALLOCK 以及触目惊心的 Java 线程被 WAIT_ FOR_ CONCURRENT_ GC 阻塞。以下图所示,经过简单统计这些 GC 消耗的时间,咱们可以得出 GC 严重影响应用启动时间的结论。函数
支付宝是 Android 系统的一个应用程序,如何可以经过影响 Dalvik 的 GC 行为来缩短启动时间呢?这个问题能够分解为两步:组件化
1.支付宝是否能影响自身 Dalvik 的行为post
2.如何改进 Dalvik,缩短启动时间
第一个问题答案是确定的,Android 系统的设计思路是每一个 Android 应用程序都有独立的 Dalvik 实例,应用启动后能够修改本身的进程空间里的代码和数据,所以支付宝经过修改内存中的 Dalvik 库文件 libdvm.so 影响 Dalvik 的行为。
第二个问题的难点在于投入产出比:修改进程空间的代码和数据是面向二进制,难度远远大于源代码,也就是说稍微复杂的Dalvik改进工做是不可能的。
基于以上两点,提出了一种设想:启动时 GC 抑制,容许堆一直增加,直到开发人员主动中止 GC 抑制或者 OOM 中止 GC 抑制,这是一种"空间换时间"策略,用更多的内存消耗来换取启动时间的缩短,这种策略可行有两个前提:一是设备厂商没有加密内存中的 Dalvik 库文件,二是设备厂商没有改动 Google 的 Dalvik 源码(或者少许的改动),理论上经过白名单的方式能够覆盖全部设备,可是实现和维护成本都很是高。
GC 抑制的前提是 Dalvik 比较熟悉,知道如何改变 GC 的行为,解决方案大体以下:首先在源码级别找到抑制GC的修改方法,例如改变跳转分支,其次,在二进制代码里找到 A 分支条件跳转的"指令指纹",以及用于改变分支的二进制代码,假设为 override_A,应用启动后扫描内存中的 libdvm.so,根据"指令指纹"定位到修改位置,而后用 override_A 覆盖,这里须要注意的是,"指令指纹"的定义须要有一些编译器和 arm 指令集知识,实现 GC 抑制主要实现了如下 4 个部分:
取消 softlimit 检测
取消 GC 线程的唤醒
取消 GC 例程函数
OOM 中止 GC 抑制的实现
取消 softlimit 检测:
取消 softlimit 检测的目的是最大限度的分配对象,下图为 softlimit 检查对应的 arm 指令片断,位于 dvmHeapSourceAlloc 函数中,OXE057 对应于"return NULL"的分支,若是咱们想永远不进入"return NULL"分支,能够改变 cmp 指令的结果,在具体实现里咱们把"0X42"做为"指令指纹"来识别并且修改成 "cmp r0, r0",这样就能够实现取消 softlimit 检查。
取消 GC 线程唤醒的目的是防止 GC 线程频繁唤醒致使的线程抖动。下图是对应的 C++ 代码和 arm 指令片断,这段代码一样位于 dvmHeapSourceAlloc 函数中。在具体实现里咱们会依次扫描 libdvm.so的 dynstr、dynsym、rel.plt 和 plt 区域获取 pthreadcondsignal@plt 的地址,而后遍历 dvmHeapSourceAlloc 中的全部分支跳转,计算跳转目的地址。
若是发现 pthreadcondsignal@plt 和当前分支跳转目的地址配置,擦除这条指令便可。
取消 GC 例程函数采用钩子技术来实现,咱们将 GC 抑制封装成了两个 native 接口 doStartSuppressGC 和 doStopSuppressGC;而且进一步封装为 JNI 接口,便于开发者在 Java 里调用。通常的应用方式是,开发者经过日志看到支付宝在某个场景会触发大量的 GC 且这个 GC 影响用户体验(响应时间慢或者动画卡顿),而后在这个场景先后插入 doStartSuppressGC 和 doStopSuppressGC。
以支付宝冷启动场景为例,咱们在容器 Quinox 的 attachBaseContext 函数里插入 doStartSuppressGC,在首页加载结束时插入 doStopSuppressGC。
若是仅仅考虑在支付宝启动过程当中抑制 GC,不须要考虑 OOM 中止 GC 抑制的实现,由于支付宝启动不足以触发 OOM。可是咱们但愿 GC 抑制成为一个基础模块,可以应用到更多场景中。若是程序在调用 doStopSuppressGC 前触发了 OOM,则须要在 OOM 发生前中止 GC 抑制。和前面简单的改变分支跳转方向不一样,须要在 OOM 发生前注入一个新的的分支跳转,这个新分支的代码由咱们来实现。新分支主要功能是,调用 doStopSuppressGC,而后去掉注入的新分支,最后跳回 Dalvik 执行 OOM。
当条件不知足时直接返回,达到取消 GC 的目的;
条件知足时,取消钩子且执行原来的 dvmCollectGarbageInternal。
实现中使用了开源的二进制注入框架:github.com/crmulliner/… 。
这里须要注意的是,在热点函数里使用这个框架提供的 pre_hook 和 post_hook 的性能开销很是大。本文里的设计只会用到一次 pre_hook,因此不存在性能问题。
看到的这里读者可能会问,这种经过“指令指纹”的方式靠谱么?个人答案是,漏判不影响正确性,误判理论上存在但几率极小(误判指“指令指纹”定位到错误代码位置)。即便误判发生了,咱们还有最后一层保障——基础架构组同窗实现的容灾机制。当误判致使程序异常没法完成正常启动时,重启支付宝并且在后续的启动中直接放弃 GC 抑制。
上图的启动时间的数据是在内部的 Android 4.x 测试设备上得到的(没有标注 release 表示 debug 版本)。从图表上来看,支付宝客户端的启动时间缩短了 15%~30%。
本人Java开发4年Android开发5年,按期分享Android高级技术及经验分享,欢迎你们关注~(喜欢文章的点个赞鼓励下叭~谢谢。)
Android前沿技术—组件化框架设计
BAT主流Android高级架构技术大纲+学习路线+资料分享
架构技术详解,学习路线与资料分享都在博客这篇文章里《“寒冬未过”,阿里P9架构分享Android必备技术点,让你offer拿到手软!》 (包括自定义控件、NDK、架构设计、混合式开发工程师(React native,Weex)、性能优化、完整商业项目开发等)