祝大家新春快乐,技术干货来袭。不知道各位还记得许久之前分享过的Finder吗?记得我们在QQ的零人力内存测试的实践吗?之前的那个专家叫“Finder”,基于MAT改造,造就了许许多多的新功能,不过已经有点老了,LeakCanary都出2了,而且还用kottin重写,性能和功能都有了质的飞跃。对于有技术追求的我们,怎敢落后,至此,这位的内存分析专家已经融入到了我们的分析云之中。下面从技术角度我们来揭开这位技术专家的秘密。
背景,追赶QAPM的愿景
QAPM原有Android内存快照分析是基于那个颇具历史感的MAT的命令行版本开发的。MAT到现在都依旧是最最强大的内存快照分析工具,就是他那个类SQL的查询能力灵活性就已经甩很多工具N条街。但是我们是个基于大数据的监控平台,我们用大数据来帮助研发聚焦问题根因的愿景,MAT的数据处理性能明显赶不上我们。后面我们发现了开源项目LeakCanary的Shark Android Extension更新,虽然功能有点简单,能处理部分安卓内存泄露,很简单内存触顶分析模块,但是用kottin重写,传说性能是以前的3倍。为了让技术赶上我们的愿景,我们切换到了Shark。下面我们从两个维度来说说,我们基于Shark如何进一步地性能优化,功能上,我们对其进行强化,加入图片重复,图片超尺寸,字符串重复,对象重复分析与问题引用链聚类等更复杂的Hprof分析。
也许是基于Shark从代码层面的终极性能优化
- 分析源码问题
我们在分析Shark的源码的时候,发现了下面一些可以优化的问题,
- 原生的堆对象代理体系索引较少,大部分操作使用Lazy Loading甚至为顺序查找,对一些要进行局部统计的操作极端不友好,耗时相对很长。
- 不保证读取线程安全,多个分析无法在一个索引上同时进行。
- 使用Okio Position实现Lazy Loading,需要与IO进行交互,甚至在读取时都要创建一票对象,对GC造成压力,分析速度大大降低。
- 对象代理体系封装死板,类型系统不够简洁,没有从外部穿透的灵活性,进行复杂业务分析困难,改写也较为困难。
根据这些问题,我们的优化目标聚焦下下面三点,
- 大部分对象查找时间复杂度为O(1)
- 建立索引后必须保证无锁的并发
- 更简洁,高效,且优雅易用的代理对象体系
- 性能优化: Eager Loading
在具体实现思路上,我们在索引建立上使用了自己的一套体系,并且拥有全新的对象代理。
与shark不同,我们采用了较为激进的Eager Loading,对分析中常见的操作都建立了索引表,保证分析器查找取用数据的速度。这样一来也可以保证在索引建立完成后,所有的读取都是线程安全的,我们可以尽情的利用多核处理器的能力。
在另外一个层面上,根据业务的实际需求,我们针对代理对象的最短引用链获取做了特别的处理。即在分析时就将最短引用链求出,而不必像原有shark那样在用到时再进行计算。且在实际业务中要获取谁的引用链是无法预知的,这就造成了一个碰运气的问题:如果对象在BFS中遍历处于靠后的位置,或者是其根本从gc root不可达,再加之老方案遍历时是通过访问字段,而字段的加载又是极大可能要触发IO的。这样一整套组合拳下来,整个分析体验就会变得尤其糟糕。新Hprof通过牺牲一些内存,换取高速的引用链获取,极大地提高了体验。
优化前:
优化后:
- 性能优化:新索引系统
在初步的版本中,由于没有引用链分析的加入以及使用的hprof较为简单,除开启动预热时间,我们没有发现特别突出的性能问题。然而在加入了引用链分析后,甚至在简单hprof中多个分析器并行获取引用链也会消耗大量的时间与内存。
发现问题
在上一阶段中,我们发现由于引用链的并行获取,造成了时间的大量消耗与内存的飙高。问题在哪呢?
经过分析,我们得出一个结论,由于当时仍然是处于shark的体系之下,其线程不安全的读取让整个支持并行的策略看起来既滑稽又无奈:为多个分析器分配多个hprof对象,并且分别并行构建。这也直接导致了我们很难在进行分析前就将统一的最短引用链求出,当然其代码封装的高度不灵活性也是阻力的来源。
不仅如此,在原有体系下针对对象的全盘统计也是极为痛苦的,通过Profiling我们发现大多数时间都被耗费在了Okio与磁盘的读取交互上,让人无法接受。诸多不满之下,更换到一个新的索引系统的想法诞生了。
解决问题
在设计全新索引和代理体系时,我们尽可能将常用的查询通过映射缓存起来,例如类型名到代理类型对象等,此等操作在原有的索引下是实打实的O(n)时间消耗。这使得我们的任何统计操作时间被大大缩短。
并且针对到以后可能出现的复杂分析,我们特地为对象缓存了一个可达表与对应的可达性类型(实例字段,静态字段,JNI Local等)。
同时我们也借助上面的可达表进行对象最短引用链的构建,以一定的内存牺牲来使得引用链获取是无需任何时间的。
优化成果
功能强化,从内存分析小白到内存分析专家
在Android系统中,Java的语境下,那些内存分析小白就只是知道Activity内存泄漏,外网也有一堆这样的文章。Shark的核心分析能力,针对的也是Activity内存泄漏。好,我先来端正下概念。
Java没有真正意义上的“内存泄漏” = Memory Leaks 为什么这么说呢?
因为我们在C语言中的内存泄漏,更多是指无法释放的内存。而Java的“内存泄漏”都有明确的引用关系,怎么可能无法释放呢?如果没有了与GC Root的间接或者直接的引用关系,就会被GC回收。有点深,是不是没看懂。我们结合Activity内存泄漏来再次理解下。
每个还在内存中的Activity的实例,如果有引用关系就是泄漏,那么每个Activity都是泄漏,因为他都有被GC Root引用。这里肯定漏了些什么? 为什么leakcanary要监听Activity的生命周期呢?因为这个判断内存泄漏他们添加了一个前提,就是这个Activity的实例走到了Destory了,他应该被GC,但是并没有。
说到这里,我们不妨再抽象下。其实Java的内存问题的核心是,“应然”与“实然”之间矛盾,正如Activity被Destory,他应该被释放,但是实际他没有。来,我们可以放飞下自己的思维了
- 内容一样的内存实例,不应该重复出现,实际出现了
- 图片的内存占用应该依据屏幕尺寸,但实际超出了
落地到实处,我们在原有的泄露基础上,我们加入了四个对内存优化具有针对性的分析器:
- 字符串重复
- Bitmap重复
- Bitmap超尺寸探测
- 普通对象重复
除此之外,我们还强化了引用链的分析能力
除开泄露分析器,其他分析器也充分利用上了预加载的最短引用链信息,通过在一组内分析引用链的相似段,找出最普遍的引用链特征,精准定位群体事件的问题所在
让专家真正融入到QAPM中
在我们的日夜兼程的努力下,它完整地融合到了QAPM之中。提供更详细的信息:GC引用链,图片的预览,尺寸,像素通道,字符串的内容等等;并且配合提单系统的修复闭环。下面晒晒界面,也欢迎大家试用。
老页面
列表
详细信息
详细信息
新页面
基于精准的引用链聚合问题,让研发能借助大数据聚焦核心问题
详细的个例信息,助力问题分析
下半部GC引用链
图片预览:可放大查看,直观检查图片尺寸是否合适,是否可以使用RGB565
提单内容自动添加引用链等详细信息
总结,专家vs小白
新内存分析专家 vs. 小白(旧版与LeakCanary 2)
新内存分析 | 旧内存分析 | LeakCanary 2 | |
---|---|---|---|
分析项 | 多样化,根据分析器制订 | 少量分析,根据规则制定 | 仅有泄露 |
分析结果 | 针对不同类型有专门化的详细信息(如图片的尺寸,像素信息等) | 无 | 无 |
便利功能 | 拥有图片预览导出,字符串内容预览等便捷功能 | 无 | 少量 |
后续规划
虽然目前已经取得了一些成果,但这还远远不够。
- 我们需要更多的分析器加入,如对于普通集合类型的低效利用(过短或者持有过多的空引用),引用值类型的分析(java.lang.Integer)等。
- 导出更多分析信息(例如针对Bitmap在不同Android版本的信息获取),来更好的定位内存中的问题所在。
- 美化信息的输出,提供更加易读,准确的结果。
- 考虑提供演进更优的框架设计,获取更好的性能来提升我们的分析体验。
- 考虑使用更适合模拟大量小对象的语言进行重写
想了解更多QAPM详情,请咨询:QAPM