luyaran的博客

一名php小菜鸟的成长故事

如何降低App的待机内存

越用越卡为哪般——如何降低App的待机内存(一)

在智能手机兴起的这几年中,我们体验到了手机内存从256M到4G的巨大变化,进程可用的内存也从仅有16/32M到现在可以使用2G以上的内存。与此同时,应用的功能也日益复杂,也有跟多的进程在同时运行,需要协作和互相切换的应用越来越多。

在硬件资源增长后,应用开发者们会尽量使用这些资源来实现更多的功能和效果,因此我们面对着各种大量消耗内存的应用,依然会感觉到内存是稀缺资源。我们任然需要需要每个应用开发者了解内存的消耗情况,并尽量节约使用内存。

1.1 新手入门

当软件实现了新功能后,准备发布版本前,往往需要进行一轮性能测试以确定没有性能问题,这类测试通常包括功能的流畅度,电量消耗和内存使用情况等。

由于内存组成的复杂性,实际上并没有简单通用的方法就能够发现所有的内存问题。下面的章节里,我们会围绕一组案例展开,通过对案例的分析讲解各种内存测试的工具和方法。这些例子都是从真实的测试案例中提取的,经过加工后使得问题表现的更加明显。

接下来我们以一个最常见的内存泄漏开始,作为最典型的内存问题,类似的情况可能在无数应用的无数版本中出现过,而且还会不断的在新版本里出现。对于这样的问题,我们必须要准确识别出来。

在大部分应用中,经常会有一类功能是需要加载附加资源的,比如显示从网络下载的文本或图片。这类功能往往需要在内存中存放要使用的资源对象,退出该功能后,就需要将这些资源对象清空。如果忘了清理,或者是代码原因造成的清理无效,就会形成内存泄漏。我们的测试任务就是保证功能的正常,并且不会有遗留的内存对象造成泄漏。

要开始进行性能测试,测试工具是必不可少的。我们一般都会优先使用SDK/IDE自带的工具,因此首先会想到的工具就是和IDE集成在一起的Android Device Monitor/Android Studio了。

大多数情况下,功能代码都是由Dalvik虚拟机里执行的Java代码实现的,因此主要的内存消耗也是由Java代码使用new分配的内存。Android Device Monitor和Android Studio能够方便的观察Heap Alloc部分的大小,进行初步的统计,还能够观察到GC发生时的内存变化情况。


图1-1 使用DDMS观察应用的内存消耗


图1-2 使用Android Studio观察应用的内存消耗

在图1-1中,我们能够看到应用当前消耗了多少内存,以及各种不同类型对象的初步统计。在图1-2中,Android Studio更进一步的将内存数据进行了图形化,这样就能过方便地看出GC(垃圾回收)情况和明显的内存趋势。如果存在明显的内存泄漏,那么在图中就会表现为随着功能的反复使用,内存值不断的升高,即使出现GC也没法降下来,如图1-3所示。


图1-3典型的内存泄漏

发现了内存泄漏,通常就可以交给开发去处理了。但我们能做的并不只是丢一个问题描述和复现路径过去,而是利用手头的工具,获得一些更详细的数据,能够使大家更快的定位和解决问题。这样分析内存获得详细数据的首选工具就是Eclipse Memory Analyzer(MAT)了。

1.1.1 使用Eclipse Memory Analyzer(MAT)进行内存分析

Eclipse Memory Analyzer(MAT)是使用非常广泛的Java内存分析工具,功能强大。已经有很多关于它的详细教程,在本书中就不再细述用法。本节内容主要介绍使用MAT在分析Android应用时的一些常用技巧。

通常我们用MAT打开hprof文件后,能够在首页看到Top Consumers和Component Report等功能,使用这些功能能够快速定位一些大块的内存消耗。但对于Android应用的hprof文件,我们在使用了Top Consumers统计使用情况后,往往只能看到如图1-4所示的情况:

图1-4 使用MAT分析内存构成

系统的资源类占据了很大一部分的内存,而其余的前几名也往往是系统类。这是由于从虚拟机角度不会区分系统框架和应用自身的对象,后面的1.4.3节会详细说明出现这种现象的原因。

为了去除这部分对分析的干扰,我们在用Android  SDK提供的hprof-conv转换时需要增加一个参数:

hprof-conv [-z] <infile><outfile>
-z: exclude non-app heaps, such as Zygote

另一种可替代的方法是使用OQL。如果hprof文件是已经转换过的,可以在数据中寻找应用的Application类对象,将对象地址转换为10进制后输入以下查询语句:

select * from instanceof java.lang.Object s where s.@objectAddress > 1107296256

使用-z参数转换或OQL查询后得到的对象集合就只包含应用代码分配的部分了。在此基础上使用MAT提供的Top Consumers和Component Report等功能就能够得到比较准确的结果。如图1-5所示,没有了系统类所占内存的干扰,只有应用自身代码创建的对象,对于发现内存问题比较有帮助。


图1-5分离之后再次分析内存构成

对于一般的内存泄露类问题,使用以上方法后通过MAT提供的分析报告就很容易就会识别出来。在我们以往的测试经历中,用这种方法发现了上百次的内存问题。这些内存往往是加载后忘了释放的Bitmap,临时生成的byte数组和文件缓冲区,包含Handler的Activity等等。

接下来我们看一个真实的应用测试案例。在这个案例里,有些位图在使用完之后由于种种原因,一直没有销毁而存在ImageLoader里,使用一段时间后ImageLoader会变得越来越庞大。使用上面介绍的方法去除了系统的影响后,MAT的泄露报告给出了结果,如图1-6所示,ImageLoader消耗了接近1/3的内存。


图1-6 MAT识别出来的问题

有了这样的数据,接下来就可以结合图片追踪代码,看引用到ImageLoader的代码部分哪里有问题,从而快速的修复问题。

如何降低App的待机内存(二)——规范测试流程及常见问题

1.2规范测试流程及常见问题

最开始进行内存测试时,我们可能还有些摸不着头脑,试着找了些工具,看了看教程就开始动手了。有时候因为问题比较明显,就真的发现了问题。再之后遇到类似的测试需求,我们就会按上次的经验去做。有时候可能发现问题,也可能发现不了,还有些时候发现是甚至是在白费功夫。因为随着明显的问题逐渐被找出来,剩下的有都是更加复杂而不太明显的问题,甚至有些更是可以归属到优化范畴或者产品策略之内,而不再是简单的内存问题。

随着经验的逐渐增加,我们逐渐意识到,以前的很多测试方法都是随机乱测。对于较为成熟的软件,这类方法的测试有效性往往比较低,运气好了才会遇到问题。如果是较深层次的问题要么遇不到,要么遇到了也找不出原因。因此,有必要总结出一套成熟的流程方法,能够考虑到各个方面,提高测试的有效性。

1.2.1测试流程

由于内存测试属于性能测试,Android系统又和Linux有很多相通之处。因此我们可以参考常见的Linux性能测试方法和指标,来制定客户端性能测试方案。常见的测试方法包括Monkey/UIAutomator类的常规压力测试,大数据/操作的峰值压力测试,长时间运行的稳定性测试等。这些方法都可以叠加在内存测试的方案中,观察这类场景下的应用内存情况,经常能够发现类似内存泄漏或OOM的问题。

参考了常见性能测试的方案,以及总结了以往对内存性能测试的经验后,我们总结出了一套进行内存测试的经验流程,供读者在制定测试计划时参考。

1.代码

通常用来进行内存测试的版本是纯净版本,不应该附加多余的Log和调试用组件。例如有些情况下,为了测试界面延迟/函数执行时间等性能,会加入一些桩点代码。在内存测试中这些代码是不必要的,它们可能会分配临时内存,引起更多的GC,导致应用出现运行缓慢、卡顿等现象。

2.测试场景(来自于经验&开发)

测试场景通常有两类,一类是当前有新开发或改动的某项功能,需要对该功能进行性能测试。因此测试场景主要针对该功能组织,包括功能的开启前,运行,结束后等测试点。

另一类是整体性能,考察应用的常见场景,在综合使用情况下的性能指标。测试场景应当包括启动后待机,切换到后台,执行主要功能,以及反复执行各功能后。

在各类场景中,经常做为测试重点的有:

  • 包含了图片显示的界面

  • 网络传输大量数据

  • 需要缓存数据的场景

3.场景转换成用例

选取了测试场景后,用例设计也要考虑内存测试的特点。一些常见的方法是:

  • 结合场景比较操作前后或不同版本的内存变化

  • 显示多张图片的前台进程

  • 多个场景来回切换

  • 长时间运行进程的内存增长

4.执行

由于GC和广播机制的存在,应用内存通常都在不停得波动,幅度可能会达到几百K,因此执行时需要考虑这种情况。在采集数据时,需要多次采集并计算平均值。

执行完成中,我们就可以根据数据进行比较初步的分析以确定方向。一方面是我们熟悉的Dalvik Heap部分,既由Java代码直接分配的内存,可以通过IDE直接观察到使用情况,也可以使用MAT进行细致的分析。

另一方面,假如我们发现Dalvik Heap没怎么增长,而其他部分增长了许多,这种情况下的分析就要复杂一些,我们留待后面的章节再说。

1.2.2 Dalvik Heap的常见问题

随着测试的执行,随之而来的就是一大堆产生的数据,对产生的数据进行分析,找出可能存在的问题,以及问题可能的原因是接下来的重点。

由于大部分Android应用是Java代码开发的,所以Dalvik Heap内存出现问题也是最常见的情况。常见的现象有以下几种:

  1. 随着功能的反复执行,Heap内存一直在持续增长。这种情况通常是出现了内存泄露,这种情况最适合用LeakCanary等泄露检查工具进行白盒测试分析。

  2. 代码执行时出现了频繁的GC,Heap Alloc内存大幅度波动。这种情况通常是分配了许多临时变量或数组,随后又被迅速回收,这种情况在确定具体场景后适合使用Heap Viewer / Allocation Tracker等工具来查看具体分配的对象。

  3. 每次启动应用后,Heap内存相比以前版本稳定增长。这种情况通常出现在启动后待机或使用某功能后,可能是由新功能及代码改动引入的固定内存增长。这种情况适合获取Heap Dump后进行多版本或功能使用前后的对此,能够迅速找到增长原因。

  4. Heap Alloc变化不大,但进程的Dalvik Heap PSS内存明显增加。这种情况比较少见,是由于分配了大量小对象造成的内存碎片,在后面的章节里会详细讲解,具体内容请见下一节。

1.2.3 示例

1.1节,我们已经介绍了出现内存泄漏时的问题现象及分析方法,在这里我们再以一个真实的例子介绍常见的几种内存问题和分析方法。

这是发生在手机管家4.x的某个版本上的案例,新版中加入了一些功能,开发人员估计新功能可能会分配几十到几百K的内存,因此我们来进行内存方面的测试验证。当新功能的代码合入后,我们发现应用启动后的内存增长超过了2M,这可大大超出了所有人的预期,一定是有什么地方出了严重的问题。

由于新加入了好几个功能,因此要逐个去排查。如果某个新功能的代码都在同一个package下,那么就可以使用MAT的过滤功能来验证这部分代码是否使用了内存。如图1-7所示:

图1-7 使用MAT的过滤功能

经过一番筛选排查,发现内存中多出了一些新对象,多消耗了约300K内存,目前这并不能解释内存增长了2M的原因。但仔细检查多出来的对象并清理掉不用的部分也是有帮助的。

经过检查,这部分内存是其他新功能使用的。对此我们需要进一步确认,这些对象是否是有用的,还是临时创建的。对于临时创建不再使用的可以主动销毁,而保存着信息将要用到的也可以进行进行压缩裁剪,以进一步减少占用的内存。

在以上排查中,我们确实发现了一些问题,但将一些不用的对象清理后再执行测试,总体内存并没有出现明显的减少。现在看来,Dalvik Heap里分配的内存并没有增加许多,说明问题是不能只在Dalivk Heap里就解决的。也许是别的部分出现了问题?接下面我们就继续深挖下去。

1.2.4 新的问题

经过上一轮的优化,在内存监视器里新版本的Heap内存表现已经比较好了,新功能只消耗了几十K到上百K内存。但是要注意的是,Heap内存并不是应用的全部,我们在设置或其他管理工具里看到的应用内存是应用整个进程的内存使用量。也有可能出现Heap部分完全没有增长而其他部分增长的情况。

要观察进程的内存使用情况,就需要用到其他的观测工具,Android里最常用的观察进程内存的方法就是dumpsys meminfo <package name|pid>命令。

对我们的新版应用执行该命令,能够得到以下的输出结果:

在以上输出结果中,左边Pss Total列的数据标识进程各部分对真实物理内存的消耗,左下角的TOTAL值就是我们在各种管理工具里看到的应用内存消耗。

而Android Studio等工具里显示的内存值,在这里是Dalvik Heap Alloc部分。根据以上的数据,我们可以看到Dalvik Heap和Heap Alloc不是相等的,而且除了Dalvik Heap之外,还有其他很多部分也会消耗内存。

这时候我们再对比一下旧版,看看是否也如此:

这时候就会发现问题了,Heap Alloc没增加多少,但Dalvik Heap Pss却涨了许多。而其他部分基本保持不变或有少量增长。可见问题还是出现Dalvik Heap部分,但只靠检查分配的对象是看不出来问题的。

Java代码的内存分配和释放都是由虚拟机管理的,那么这个问题会是虚拟机的问题吗?

如何降低App的待机内存(三)——探索内存增长的原因

1.3新问题的进一步挖掘

在上一节里,我们介绍了内存测试的基本流程,讲述了如何发现并处理简单的内存问题。对于Dalvik Heap部分总结出了一些常见的问题模式,以及如何使用工具识别和处理这些常见的内存问题。

当简单问题不再是问题的时候,我们就会开始遇上一些奇怪问题了,类似于下面这些:

“我们这个版本引入了一个挺简单的库,内存就涨了2M”
“这些代码只是初始化了几个对象,还没有开始用呢”
“我只是改了一行代码,没有创建新对象”
“我一行代码都没改,怎么会涨呢”

这次出现的问题就是这样这一类问题,新版本的Dalvik Heap Pss内存出现了2M左右的增长。但Dalvik Heap Alloc只增长了273K的情况下。而从Dalvik Heap Free也能看出大部分增长的内存是空闲状态的。

经过一段时间对问题的观察,我们有以下几点发现:

  • 经过较长时间待机后也没有被释放回系统。

  • 有几处代码会导致内存增长,只要将这些代码屏蔽掉,内存情况就下降到正常水品。

  • 这些代码分配的内存并不多,甚至有些地方是不需要分配内存的。

  • 有些代码并不是这个版本新加入的,已经存在较长时间了。

  • 使用裁剪功能的方法编译并分析内存后,基本可以确定是新加入代码消耗了内存,但并没有内存泄漏,代码经过review也没有发现问题。

这个结果让我们陷入了困惑,常用的方法找不出问题,说明有更深层次的原因。接下来要从更底层的Dalvik虚拟机寻找问题。

1.3.1 Dalvik Heap内部机制

为了弄清楚为什么DVM占着内存不释放,我们阅读了DVM分配内存部分的代码。位置在Android源码的dalvik/vm/alloc下,约255K。分析出的主要流程如下:

1) DVM使用mmap系统调用从系统分配大块内存作为Java Heap。根据系统机制,如果分类的内存尚未真正使用,就不计入PrivateDirty和PSS。例如图1-8,Heap Size/Alloc很多,但大部分是共享,实际使用的较少。所以反映到PrivateDirty/PSS里的内存并不多。

图1-8 共享内存较多的进程

2) New对象之后,由于要向对应的地址写入数据,内核开始真正分配该地址对应的4K物理内存页面。

Alloc.cpp, 176行起:

图1-9 DVM虚拟机分配内存的代码

3) 运行一段时间后,开始GC,有些对象被回收了,有些会一直存在,如图1-10所示。

图1-10黑点表示的内存会被回收

4) 在GC时,有可能会进行trim。即将空闲的物理页面释放回系统,表现为PrivateDirty/PSS下降。

HeapSource.cpp, 431行:

图1-11 释放内存回系统的代码(一)

HeapSource.cpp,1304行:

图1-12 释放内存回系统的代码(二)

1.3.2 问题所在

在了解DVM分配释放内存的机制后,根据dumpsys观察到的现象,猜测可能出现了页利用率问题(页内碎片)。如图1-13所示,第一行:在开始阶段,内存分配的较满。第二行:经过GC后,大部分对象被释放,少部分留下来。

图1-13产生内存碎片

这种情况下可能会产生的问题是,整页的4K内存中可能只有一个小对象,但统计PrivateDirty/PSS时还是按4K计算。

在通常的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内存碎片

为了能够找出有问题的代码,我们将上一步得到的数据继续处理。取出所有使用不满2K的页面的内存块地址,再使用OQL将地址导入到MAT中,分析地址对应的对象是什么。如图1-15所示就是将地址重新导入到MAT中得到的对象列表了:

图1-15内存碎片页中的对象

在这里基本就能看出来是哪些对象造成了内存的碎片化,数量比较多的前几个类的自然嫌疑比较大,可以先对前几个类的相关代码进行分析。也可以对这些代码进行针对性的内存测试,观察内存情况。

通过对生成这些对象的代码分析和模拟实验,我们还原出问题的基本过程:

  • 生成对象过程需要较多的临时变量。

  • 批量生成过程中,由于还有空闲内存,虚拟机没有做GC。

  • 完成后才进行GC,清除了所有的零时变量,留下碎片化的内存。

下图是造成这个问题的类似代码,执行这段代码将会在内存中形成很多碎片,造成很高的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系统的测试经验,了解内核及进程相关的知识,熟悉常用工具。

  • 内存分配的最小单位是页面,通常为4K。

对于开发人员,以下两个经验也许能有帮助:

  • 尽量不要在循环中创建很多临时变量。

  • 可以将大型的循环拆散,分段或者按需执行。

如何降低App的待机内存(四)——进阶:内存原理

1.4进阶:内存原理

在上一节里,我们通过深入调查Dalvik虚拟机的方式,解决了Dalvik Heap Pss消耗内存过高的问题。除了Dalvik Heap Pss部分,应用还有其他许多消耗内存的部分。本节里我们就主要介绍其他这些部分的内存是如何被分配和消耗的。

同样以我们的应用为例,在几个版本之后,新加入了一个缓存功能。缓存功能会预先取一些手机的信息,并放在内存中供其他功能使用,这样可以减少后续功能的消耗,加快运行速度。

有了之前的经验,我们自然会想到不能简单粗暴的将所有缓存一次生成,这样可能会产生大量的碎片,因此需要选择一种合适的策略来进行。在选择新功能的缓存策略时,内存测试也同样有用,通过对不同策略的测试,决定那种策略比较有效,并且消耗内存比较少。

在测试过程中我们发现,随着使用不同的策略,Dalvik Heap部分会随之增减。与此同时,不同策略执行代码的时机也会使dalvik other和dex mmap的内存消耗变化。总结规律如下:

  • 不生成缓存时,Dalvik Other和mmap会随之下降。

  • 按需生成缓存时,即使只生成一条记录,Dalvik Other和mmap会增加。

  • 生成多条缓存记录时,Dalvik Other和mmap会在开始增加,然后一直保持不变。

  • Dalvik Other不会下降,mmap偶尔会下降。

通常我们只是大致了解到,Dalvik Other和mmap和代码数量相关,越复杂的应用这部分内存就越多,并没有进行过定量的分析。但现在随着对Dalvik Heap部分的优化,我们发现Dalvik Other和mmap在内存中的比重越来越大。在这个版本里,占总内存的将近一半,不能再置之不理,而是要寻找办法对这部分内存进行优化。

对于这些不熟悉的部分,我们也首先要先去了解背后的原理,才能够针对性的去研究这些内存是如何被消耗的。

1.4.1 从物理内存到应用

我们首先要了解系统的内存机制,搞清楚物理内存是如何被分配到各个进程的,以及共享内存的机制等等。理解内存机制对测试及优化都有很大帮助。

图1-14 Android架构

根据Google提供的Android整体架构图,如图1-14所示,可以看到Android系统是基于Linux内核的,因此底层的内存分配及共享机制与Linux基本相同。但由于Android是为移动设备设计的,所以整套架构为了符合移动设备的特性,需要有较低的内存及能耗需求。因此Android只使用了Linux内核,不使用传统Linux系统的组件。这些组件虽然功能强大,但是较为消耗系统资源。Google开发了若干较小的组件,例如将庞大的glibc换为bionic库,使用sqlite数据库等。Android还扩充了许多内核机制和实现,其中对内存影响较大的是Ashmem和Binder机制。

在Ashmem及COW(Copy-On-Write)机制的基础上,Android进程最明显的内存特征是与zygote共享内存。为了加快启动速度及节约内存,Android应用的进程都是由zygote fork出来的。由于zygote已经载入了完整的Dalvik虚拟机和Android 应用框架的代码,fork出的进程和zygote共享同一块内存这样就节约了每个进程单独载入的时间和内存。应用进程只需要载入自己的dalvik字节码及资源就可以开始工作。

综上所述,一个在运行的Android应用进程会包含以下几个部分:

  • 共享内存:Dalvik虚拟机代码

  • 共享内存:应用框架的代码

  • 共享内存:应用框架的资源

  • 共享内存:应用框架的so库

  • 私有内存:应用的代码

  • 私有内存:应用的资源

  • 私有内存:应用的so库

  • 共享/私有:堆内存,其它部分

有了整体视角后,我们再开始深入,观察某一个应用的内存情况。在之前的测试中,我们使用系统提供的dumpsys meminfo工具来观察内存值。它能够将不同的内存消耗分类统计,输出成便于查看的格式。

但如果我们想细致的研究各部分内存的由来,只靠这个工具是不够的,但我们有必要按照系统划分各部分的方式来理解和分析内存。

通过阅读和分析dumpsys meminfo的代码,我们能够了解到Android是如何划分各部分内存的。下面我们详细讲解dumpsys meminfo工具是如何统计各部分内存值的。

1.4.2 smaps

由于Android底层基于是Linux内核,进程内存信息也和linux一致,所以Dalvik Heap之外的信息都能够从/proc//smaps中取得。

在smaps中,列出了进程的各个内存区域,并根据分配的不同用途做标识,以下是root用户使用cat /proc//smaps的一个例子:

dumpsys统计各个内存块的Pss,SharedDirty,PrivateDirty等值,并按以下原则进行了归并:

  • /dev/ashmem/dalvik-heap和/dev/ashmem/dalvik-zygote归为Dalvik Heap。

  • 其它以/dev/ashmem/dalvik-开头的内存区域归为Dalvik Other。

  • Ashmem对应所有/dev/ashmem/下不以dalvik-开头的内存区域。

  • Other dev对应的是以/dev下其他的内存区域。

  • 文件的mmap按已知的几个扩展名分类,其余的归为other mmap。

  • 其它部分,如[stack],[malloc],Unknown等。

了解了dumpsys的方法后,我们可以自己解析smaps,看看归并前各项的内存都是多少。这样能够得到比dumpsys更详细的信息,有助于分析一些问题。

首先将PSS分为以下几大类,计算各部分占比。在这个例子里,几大项是三分天下的节奏。Dalvik和Other dev内存都占了30%以上,剩下的是MMAP和Unknown。进行内存优化时不能只看Dalvik部分,需要同时评估所有的部分。

1. Dalvik:
Dalvik内存分为多个区域,meminfo统计的是所有区域累加的值。

  • Dalvik_Heap: 包括dalvik-heap和dalvik-zygote
    堆内存,所有的java对象实例都放在这里。

  • LinearAlloc: dalvik-LinearAlloc
    线性分配器,虚拟机存放载入类的函数信息,随着dex里的函数数量而增加。著名的65535个函数的限制就是从这里来的。

  • Accounting: dalvik-aux-structure, dalvik-bitmap, dalvik-card-table
    这部分内存主要做标记和指针表使用。dalvik-aux-structure随着类及方法数目而增大。dalvik-bitmap随着dalvik-heap的    增大而增大。

  • Code_Cache: dalvik-jit-code-cache
    jit编译代码后的缓存,随着代码复杂度的增加变大。

由于堆内存部分往往是应用消耗内存最多的地方,在内存优化中,最常见的方法就是减少Dalvik Heap中创建的对象,能够直接减少Dalvik Heap,并间接减少Accounting部分。减少代码会直接减少运行辅助部分。

在进行不同版本的对比测试时,我们往往会发现Dalvik Other和dex mmap出现了稳定的增长,这是由新加入的代码引入的内存消耗。

根据Dalvik虚拟机的原理,在加载class时,会根据类的变量个数及函数个数申请相应尺寸的内存,作为运行时的内部指针。这部分内存就会体现在LinearAlloc及aux-structure的增长中。随着版本的开发,应用class的数目及复杂度也在不断地增长,因此Dalvik Other部分也在不断地增长。

由于这部分内存的增长取决于代码复杂度,通常情况下并没有简单直接的方法能够降低它们的消耗。但是通过仔细分析他们的组成及原理,还是能够找出一些间接的方法降低这部分内存的,详细方法请见2.6节。

2. MMAPs:
系统会将一些文件mmap到内存中,对各个文件进行mmap的时机及大小比较复杂。Dex mmap是其中主要的内容。

应用的dex会占据较大的空间,并且随着代码增加使得dex文件变大,占用的内存也会增加。减小dex的(相当于减少代码)尺寸能够降低这部分内存占用,同时也会减少dalvik部分的内存。

1.4.3 zygote共享内存机制

在上一小节,我们介绍了应用各部分内存的含义,读者对dumpsys meminfo输出的大部分数据都能够有所理解。但dumpsys meminfo工具还会输出Heap Size/Alloc/Free部分的数值。我们知道这些数值是Dalvik虚拟机统计的内存堆的使用量,但这些数值是如何对应到Pss内存上的?比如Heap Alloc和Heap Pss往往相差不远,那他们是不是能够看做基本等同的呢?下面我们试图解释这几项数值之间的关系。

由于虚拟机运行时并不区分某个对象实例是Android框架共享的还是应用独有的,Heap Alloc统计的是由虚拟机分配的所有应用实例的内存,所以会将应用从zygote共享的部分也算进去,所以Heap Alloc值总是比实际物理内存使用值要大。

Heap Alloc虽然反映了Java代码分配的内存,但存在框架造成的失真。除此之外,进程还有许多其它部分也需要使用内存。为了准确了解应用消耗的内存,我们要从进程角度而不是虚拟机角度来进行观察。

PSS(Proportional Set Size),表示进程实际使用的物理内存,是由私有内存加上按比例分担计算的各进程共享内存得到的值。例如,如果有三个进程都使用了一个消耗30K内存的so库,那么每个进程在计算这部分PSS值的时候,只会计算10K。总的计算公式是:

Dalvik PSS内存 = 私有内存Private Dirty + (共享内存Shared Dirty / 共享的进程数)

从实际含义来讲,Private Dirty部分存放的是应用new出来的对象实例,是每个应用所独有的,不会再共享。Shared Dirty部分主要是zygote加载的Android框架部分,会被所有Android应用进程共享。通常进程数的值在10-50的范围内。

PSS是一个非常有用的数值,如果系统中所有的进程的PSS相加,所得和即为系统占用内存的总和。但要注意的是,进程的PSS并不代表进程结束后系统能够回收的内存大小。

1.4.4 多进程应用

根据我们在上一节中的描述,当一个进程结束后,它所占用的共享库内存将会被其它仍然使用该共享库的进程所分担,共享库消耗的物理内存并不会减少。实际上,所有共享使用了这个库的应用,PSS内存都会有所增加。对于一般的进程,只是共享着zygote进程的Android框架等基础部分,而通常手机使用时的应用进程数达到几十至上百,所以某个进程结束,其他进程内存增加的情况并不明显。

但对于多进程的应用来说,由于多个进程之间会共享很多内容,包括代码,资源,so库等等,因此单个进程结束造成的影响就会比较明显。以有两个进程的应用为例,进程共享着部分内存,因此当一个进程不再需要这些内存时,就会出现如图1-15所示中的场景。表现为一个进程的内存下降了,另一个进程就会明显的上升。

图1-15两个共享内存进程的内存变化

由此可见,我们在统计多进程的应用内存和进行优化时,需要综合考虑。以免出现努力优化了一个进程的内存,却造成其他进程内存增长的情况。


阅读更多

扫码向博主提问

去开通我的Chat快问

luyaran

非学,无以致疑;非问,无以广识
  • 擅长领域:
  • PHP
  • MySQL
  • shell
  • jQuery
  • js
个人分类: 杂谈
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

如何降低App的待机内存

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭