1.3.2 问题所在
在了解DVM分配释放内存的机制后,根据dumpsys观察到的现象,猜测可能出现了页利用率问题(页内碎片)。如图1-13所示,第一行:在开始阶段,内存分配较满。第二行:经过GC(垃圾回收)后,大部分对象被释放,少部分留下来。
图1-13 产生内存碎片
这种情况下可能会产生的问题是,整页的4KB内存中可能只有一个小对象,但统计PrivateDirty/Pss时还是按4KB计算。
在通常的JVM中,借助Compacting GC机制,整理内存对象,将散布的内存移动到一起。但根据DVM的代码,DVM的Mark-Sweep算法不能移动对象,即没有内存整理功能,这种情况下就会形成内存空洞。
在猜测了可能的问题后,需要验证是否如猜测原因所致,由于MAT的对象实例数据中有地址和大小信息,我们先从MAT中导出数据。
在MAT中列出所有对象实例:list_objects java.*,然后选中所有数据并导出为csv格式,如下所示:
Class Name,Shallow Heap,Retained Heap,
class java.lang.Class @ 0x41fdd1e8,16,56,
class test.bxi$3 @ 0x432501c8,0,0,
class test.aaw$c$1 @ 0x4324fef8,0,0,
class test.ds @ 0x4324fc88,8,48,
class test.bxh @ 0x4324f438,8,248,
class test.bxg @ 0x4324f248,0,0,
class test.bxd$1 @ 0x4324f028,0,0,
处理导出的csv文件,按页面进行统计,取每个对象的地址的高位(&0xfffff000),结果相同的对象处在同一页面中。最后再按每个页面所有对象的大小分类统计,绘出直方图如图1-14所示。
图1-14 对页面利用率进行分类统计
这张图就是被测应用的页面利用率分布图,左边是利用率低的页面,右边是利用率高的页面。如果发现利用率低的页面数目增加,说明小对象碎片的数量增加了。
1.3.3 优化Dalvik内存碎片
为了能够找出有问题的代码,我们将上一步得到的数据继续处理。取出所有使用不满2KB的页面的内存块地址,再使用OQL将地址导入到MAT中,分析地址对应的对象是什么。图1-15所示就是将地址重新导入到MAT中得到的对象列表。
图1-15 内存碎片页中的对象
在这里基本就能看出来是哪些对象造成了内存的碎片化,数量比较多的前几类自然嫌疑比较大,可以先对前几个类的相关代码进行分析。也可以对这些代码进行针对性的内存测试,观察内存情况。
通过对生成这些对象的代码分析和模拟实验,我们还原出问题的基本过程:
生成对象的过程需要较多的临时变量。
批量生成过程中,由于还有空闲内存,虚拟机没有做垃圾回收。
完成后才进行垃圾回收,清除了所有的临时变量,留下碎片化的内存。
下面是造成这个问题的类似代码,执行这段代码将会在内存中形成很多碎片,造成很高的Pss占用:
private Object result[] = new Object[100];
void foo() {
for(int i = 0; i < 100; ++i) {
byte[] tmp = new byte[2000];
result[i] = new byte[4];
}
}
图1-16显示了类似情况下数组的分配范围,可见数组中每个成员的内存地址都是不连续的,并且相隔很远。这种情况下就会消耗很多个物理内存页面,增加Heap Free,造成例子中的问题。
图1-16 内存碎片对象地址的例子
经验总结
根据上述的流程,我们搞清楚了造成问题的原因,并且找到了问题代码。那么应当总结一些经验,以供借鉴。对于测试人员来说,有以下三个经验:
MAT是探索Java堆并发现问题的好帮手,能够迅速发现常见的图片和大数组等问题。但MAT也不是万能的,比如这个问题的数据就隐藏在对象的地址中。
对Android测试经验来说,可能容易找到的是应用代码及框架的各种测试经验和指导,底层以及涉及性能的测试经验并不太多。这方面可以借鉴Linux系统的测试经验,了解内核及进程相关的知识,熟悉常用工具。
内存分配的最小单位是页面,通常为4KB。
对于开发人员,以下两个经验也许能有帮助:
尽量不要在循环中创建很多临时变量。
可以将大型的循环拆散、分段或者按需执行。