记录一下这个问题的分析过程,主要是分享下Eclipse MAT的使用技巧 :-)
问题现象
在长时间的稳定性测试后,经常遇到下面2类错误导致的重启,9.0上遇到的比较多的是这个java层报错:
1 | java.lang.AssertionError: Binder ProxyMap has too many entries: 20440 (total), 20272 (uncleared), 20176 (uncleared after GC). BinderProxy leak? |
而8.0上报的都是下面这个global reference overflow的NE问题:
1 | pid: 1505, tid: 2994, name: Binder:1505_B >>> system_server <<< |
问题1:Binder ProxyMap has too many entries
这个错误信息十分清晰,BinderProxy实例的数量太多,在执行一次gc后,system_server进程依然有20176个存活的BinderProxy对象。
仔细再看下异常调用栈:
1 | at android.os.BinderProxy$ProxyMap.set(Binder.java:951) |
1 | final class BinderProxy implements IBinder { |
结合代码后,了解到ProxyMap里会存放所有的java层BinderProxy对象,使用的时WeakReference,就是为了内存泄漏,我们知道,当WeakReference自身引用的对象
在没有被其他强引用占用时,WeakReference里的引用的对象就会被虚拟机在下次gc时自动回收掉。
现在既然存在BinderProxy泄漏,那肯定就是system_server进程里某个地方一直在持有着对BinderProxy对象,那么后面排查这个问题,我们就需要解决下面两个问题:
- 这20000多个BinderProxy对象都是哪些进程的Binder对象的代理?
- 谁一直在强引用着这些BinderProxy对象?
问题2: global reference table overflow
global reference的详细用途,可以参考google文档,简单来说就是native代码里需要引用java层对象,为了防止被使用的java对象被gc回收掉,需要向虚拟机注册一个全局强引用,这样虚拟机gc时即使发现这个被引用的java对象已经没有其他java层对象持有后,也不会回收这个对象,直到global reference被取消掉,下次gc才可能会回收它;
还有个local reference,一般是在一个jni方法里,临时使用某个java对象时,先注册一个local reference,目的跟global reference是一样的。
这儿system_server进程crash的原因是global reference总的数量超过51200了,正常情况下不会有这么多引用,继续看一下这个详细的错误栈:
1 | 11-19 01:25:50.692 1000 12354 12354 F DEBUG : backtrace: |
结合BinderProxy.linkToDeath()的代码:
1 | public class Binder implements IBinder { |
1 | // frameworks/base/core/jni/android_util_Binder.cpp |
1 | class JavaDeathRecipient : public IBinder::DeathRecipient |
1 | // art/runtime/jni_internal.cc |
1 | // art/runtime/indirect_reference_table.cc |
global reference泄漏,那么就需要去看一下到底这些引用指向的是谁,aosp原本的逻辑是发生这种情况时会把所有的reference信息输出到logcat里,但是由于logd进程的日志缓冲区有限,等待测试同学发现手机重启后,可能已经过了好一段时间,前面打印的信息早就被冲掉了,不过,还好同事之前进过代码,在系统重启前会把所有的global reference的信息给持久化到了dropbox里,复现后只要检查下/data/system/dropbox目录,我们就能看到:
1 | Summary: |
可以确定也是RemoteCallbackList$Callback的问题了,那再看下具体代码:
1 | public class RemoteCallbackList<E extends IInterface> { |
再回头看下linkToDeath的底层实现,native层的JavaDeathRecipient的构造函数,就比较清楚了,system_server进程的BinderProxy对象注册了太多的死亡回调,导致global reference table爆了,这个问题跟BinderProxy泄漏应该是有关联的,所以接下来只要确认这些BinderProxy具体是谁。
增加调试代码,输出BinderProxy详细信息
先看下打印global reference table的代码:
1 | // art/runtime/reference_table.cc |
BinderProxy.getInterfaceDescriptor()方法,它返回一个描述其信息的字符串,所以只要在上面的代码判断下reference指向对象的类型,如果是BinderProxy的话,则顺便也输出下interfaceDescriptor,而如果是RemoteCallbackList$Callback类型的话,也是一样地输出其内部存储的callback对象的interfaceDescriptor:
1 | void ReferenceTable::Dump(std::ostream& os, Table& entries) { |
对应的需要修改下Binder.java和RemoteCallbackList.java等文件,添加如下方法:
1 | // frameworks/base/core/jni/android_util_Binder.cpp |
1 | // frameworks/base/core/java/android/os/Binder.java |
相应的需要在native层增加getInterfaceDescriptorV2()函数
1 | // frameworks/base/core/jni/android_util_Binder.cpp |
1 | // frameworks/base/core/java/android/os/RemoteCallbackList.java |
OK,一切就续,下面就只要make framework && make libart && make lib_android_runtime, 然后把生成的so文件push到手机上对应的位置后,重启手机,稳定性测试一段事件后,连上debugger,手动执行下Debug.dumpReferenceTables(),发现依然没有打印出来的interfaceDescriptor都是空的,回看上面的BinderProxy.getInterfaceDescriptor()和BinderProxy.getInterfaceDescriptorV2()方法,它们实际都是个binder call,需要ipc到远端进程的调用Binder.getInterfaceDescriptorV2(),所以这儿可能的问题就是远端进程可能早就挂了,这个ipc肯定就失败了。
再调整
既然binderProxy对应的远端进程可能会挂掉,那么在远端进程调用system_server的接口,写Binder对象的时候,顺便把它的interfaceDescriptor写入Parcel,一并发送给system_system,然后system_server进程收到binder调用时,把远端app进程的写入的interfaceDescriptor从parcel里反序列化出来,塞进对应的BinderProxy即可。
考虑到system_server会或类似installd, sufaceflinger这类native进程进行binder call,所以像下面这样仅修改Parcel.java#writeStrongBinder()方法,还不够稳妥:
1 | public class Binder implements IBinder { |
否则可能因为序列化/反序列化时对parcel读写不一致,引入不必要的稳定性问题,那么就得换个方式,改libbinder的底层实现,因为不管是java进程还是native进程实际都是使用的libbinder:
1 | // frameworks/base/core/jni/android_os_Parcel.cpp |
1 | // frameworks/native/libs/binder/Parcel.cpp |
同样BinderProxy.getInterfaceDescriptorV2()方法,去掉其native关键字,同时删除其对应的jni实现
1 | final class BinderProxy implements IBinder { |
注:在ipc时,binder驱动在序列化binder时,如果发现操作的是BnBinder对象,即类型是BINDER_TYPE_BINDER的,则实际会写入BINDER_TYPE_HANDLE类型,这样当ipc对端反序列化时,就会构造一个BpBinder对象表示原来的BnBinder对象;如果写入的就是BINDER_TYPE_HANDLE,则binder驱动写入的还是BINDER_TYPE_HANDLE类型。感兴趣的话可以看kernel/drivers/staging/android/Binder.c#binder_transaction() 的实现逻辑
定位泄漏点
重新打包后,压测跑到6000多次,就复现了这个问题,查看转储到dropbox里的global reference table文件:
1 | BinderProxy diagnosis: total size of 21762 |
都是指向蓝牙(uid=1002)进程的BinderProxy,同时我在系统crash前也打印了system_server挂掉前的heap profile,所以接下来先借助下Eclipse MAT来统计下所有的BinderProxy
注:android上通过Debug.dumpHprofData()接口dump下来的hprof文件需要用hprof-con工具手动转一下才能由MAT打开,这个工具位于你下载的android sdk目录,见${android-sdk}/platform-tools/hprof-conv
展开所有的entry后,点击工具栏的”导出”按钮,再由shell命令排序好后,得到:
BinderProxy | 数量 |
---|---|
android.bluetooth.IBluetoothHeadset | 6583 |
android.bluetooth.IBluetoothHidDevice | 6518 |
miui.process.IMiuiApplicationThread | 6048 |
挑选一个interfaceDescriptor是蓝牙进程的IMiuiApplicationThread的BinderProxy的实例:
1 | SELECT * FROM android.os.BinderProxy s where toString(s.descriptorV2.value) = "com.android.bluetooth-u1002-p304-miui.process.IMiuiApplicationThread@c01e15e" |
有泄漏,即它没有被虚拟机回收,那么必定就有>=1的gc 跟节点还(直接或间接)持有这个被泄露的对象的引用,所以再借助MAT来分析有哪些gc根节点指向这个被泄露的对象:
可以看到这个BinderProxy是被mMiuiApplicationThreads这个SparseArray强引用着,mMiuiApplicationThreads又是被mMiuiApplicationThreadManager引用,它是mMiuiApplicationThreadManager定义在ProcessManagerService.java里mMiuiApplicationThreads这个SparseArray的size是 6143!。
结合代码发现了每个进程的启动的时候会向system_server注册一个descriptor为IMiuiApplicationThread的BinderProxy对象,主要用于MIUI的长截屏功能而在进程挂掉的时候,却没有从移除掉,所以就发生泄漏,详细代码枯燥,此处略,这一块属于进程管理相关的,直接通知相关同学进行修复了。
继续定位剩余两个泄漏项,流程跟定位IMiuiApplicationThread泄漏的是一样的:
1、标定一个泄漏的BinderProxy
2、查看它的gc roots
可以看到它被ProcessRecord.connections这个ArraySet引用着,那么下面再看下这个ProcessRecord是表示的哪个进程:
最后得到:
原来是system_server进程的ProcessRecord的问题,注意看connections.mArray的大小,有7749个!
3、确认代码逻辑:
- 翻看下对ProcessRecord.connections进行增删的地方,只有binderService和unbindService
- 到这里我们就可以判定肯定是system_server某个地方有重复绑定IBluetoothHeadset这个代表的service了,去package/apps/bluetooth目录下确认这个类是HeadsetService.java了,最终定位到BluetoothManagerService.java类里确实存在重复bind的情况,具体代码略,另外还确认到BluetoothHidDevice.java逻辑也存在问题,导致压力测试中systemui和com.xiaomi.bluetooth会出现重复bind的情况,已经跟蓝牙同事沟通,由他们进行修复并提交给aosp。
继续调查global reference overflow问题
在上面的问题都修复后,9.0上依然有global reference overflow问题,继续测试后确认是桌面频繁调用壁纸接口导致,根本原因是RemoteCallbackList.register方法实现存在问题下面这段代码可以很容易的制造出这个问题:
1 | void testGetCurrentWallpaper() { |
提交了修复patch给aosp,这类小概率问题google的研发一般处理的都不会太积极
1 |
|
小结:
内存泄露问题,平时一般都不会太关注,如果是app开发还好点,集成一下leakcannary这类工具,可以做到自动分析activity这类泄露,基本上能把潜在的泄露在外发前就解决掉;但system_server进程有点特殊,除非发生OTA,用户手动重启手机这种情况,它是一直在后台运行的,哪怕有一点泄露,也经不住几周或几个月的累积,积累到一定程度了,就是系统卡顿,app频繁被杀,最终异常重启,影响用户口碑。