大家好,欢迎来到IT知识分享网。
参考《移动APP性能评测与优化》,总结内存原理。
一、内存原理
除了Dalvik Heap Pss以外,介绍其他部分的内存是如何被分配和消耗的。
举例在几个版本之后,进加入一个缓存功能。缓存功能会预先取一些手机信息,并放在内存中供其他功能使用,这样可以减少后续功能的消耗,加快运行速度。
有了之前的经验,自然想到不能简单粗暴地将所有缓存一次生成,这样可能会产生大量的碎片,因此需要选择一种合适的策略来执行。
在选择新功能的缓存策略时,内存测试也同样有用,通过对不同策略的测试,决定哪种策略比较有效,并且消耗内存比较少。
测试中发现,随着使用不同的策略,Dalvik Heap部分会随之增减。与此同时,不同策略执行代码的时机也会使Dalvik Other和Dex Mmap的内存消耗变化。
总结规律如下:
(1)不生成缓存时,Dalvik Other和Mmap会随之下降;
(2)按需生成缓存时,即使只生成一条记录,Dalvik Other和Mmap也会增加;
(3)生成多条缓存记录时,Dalvik Other和Mmap会在开始增加,然后一直保持不表;
(4)Dalvik Other不会下降,Mmap偶尔会下降;
通常只是大致了解到,Dalvik Other和Mmap和代码数量有关,对于越复杂的应用,这部分的内存就越多,并没有进行过定量的分析。
随着Dalvik Heap部分的优化,发现Dalvik Other和Mmap在内存中的比重越来越大。
1、从物理内存到应用
首先要了解系统的内存机制,搞清楚物理内存是如何被分配到各个进程的,以及共享内存机制等,理解这些机制对测试及优化有很大的帮助。
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应用进程包含如下几个部分:
(1)Dalvik虚拟机代码(共享内存);
(2)应用框架的代码(共享内存);
(3)应用框架的资源(共享内存);
(4)应用框架的so库(共享内存);
(5)应用的代码(私有内存);
(6)应用的资源(私有内存);
(7)应用的so库(私有内存);
(8)堆内存,其他部分(共享/私有)
dumpsys meminfo观察内存值,能够将不同的内存消耗分类统计,输出成便于查看的格式。
现在需要按照系统划分各部分的方式来理解和分析内存。下面详细了解dumpsys meminfo工具如何统计各部分内存值的。
2、smaps
由于Android底层基于Linux内核,进程内存信息也和Linux一致,所以Dalvik Heap之外的信息能够从/proc/<pid>/smaps中取得。
在smaps中列出了进程的各个内存区域,并根据分配的不同的用途做标识;举例cat /proc/<pid>/smaps
dunpsys统计各个内存块的Pss、Shared_Diry、Private_Drity等值,并按照以下原则做了归并:
a. /dev/ashmem/dalvik-heap和/dev/ashmem/dalvik-zygote归为Dalvik Heap;
b. 其他以/dev/ashmem/dalvik-开头的内存区域归为Dalvik Other;
c. Ashmem对应/dev/ashmem/下所有不以dalvik-开头的内存的区域;
d. Other dev对应的是/dev下其他的内存区域;
e. 文件的mmap按已知的几个扩展名分类,其余的归为Other Mmap;
f. 其他部分,如[stack] 、[malloc]、Unknown等;
了解dumpsys方法后,我们可以自己解析smaps,看看归并前各项的内存都是多少,这样可以得到比dumpsys更详细的信息。
首先将Pss分为以下几大类,计算各部分占比。
进行内存优化时不能只看Dalvik部分,需要同时评估所有的部分。
(1)Dalvik
Dalvik内存分为多个区域,meminfo 统计的是所有区域累加的值:
其中:
Dalvik_Heap——包括dalvik-heap和dalvik-zygote。堆内存,所有的Java对象实例都在这里;
LinearAlloc——包括dalvik-LinearAlloc。线性分配器,虚拟机存放载入类的函数信息,随着dex里的函数数量而增加。著名的65535个函数限制就是这里来的;
Accounting——包括dalvik-aux-structure、dalvik-bitmap、dalvik-dcard-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)mmap
系统会将一些文件mmap到内存中,对各个文件进行mmap的时机及大小比较复杂。
应用的dex会占据较大的空间,并且随着代码增加使得dex文件变大,占用的内存也会增加。
减小dex的(相当于减少代码)尺寸能够降低这部分内存占用,同时也会减少dalvik部分的内存。
3、zygote共享内存机制
由于虚拟机运行时并不区分某个对象实例时Android框架共享的还是应用独有的,Heap Alloc统计的是由虚拟机分配的所有应用实例的内存,所以会将应用从zygote共享的部分也算进去,于是Heap Alloc值总是比实际物理内存使用值要大。
Heap Alloc虽然反映了Java代码分配的内存,但存在框架造成的失真。除此之外,进程还有许多其他部分也需要使用内存。
为了准确了解应用消耗的内存,我们要从进程角度而不是虚拟机角度来进行观察。
Pss表示进程实际使用的物理内存,是由私有内存加上按比例分担计算的各进程共享内存得到的值。例如有三个进程都使用了一个消耗300KB内存的so库,那么每个进程在计算这部分Pss值的时候,只会计算10KB。总的计算公式是:
Dalvik Pss内存=私有内存 Private Dirty + (共享内存 Shared Dirty / 共享的进程数)
从实际含义来讲,Private Dirty部分存放的是应用新建(new)出来的对象实例,是每个应用所独有的,不会再共享。
Shared Dirty部分主要是zygote加载的Android框架部分,会被所有Android应用进程共享,通常进程数的值在10-50的范围内。
Pss是一个非常有用的数值,如果系统中所有进程的Pss值相加,所得和即为系统占用内存的总和。
但要注意的是,进程的Pss并不代表进程结束后系统能够回收的内存大小。
4、多进程应用
当一个进程结束后,它所占用的共享库内存将会被其他仍然使用该共享库的进程分担,共享库消耗的物理内存并不会减少。
实际上,对于所有共享使用了这个库的应用,Pss内存都会有所增加。
对于一般的进程,只是共享着zygote进程的Android框架等基础部分,而通常手机使用时的应用进程数达到几十个甚至上百个,所以某个进程结束后,其他进程内存增加的情况并不明显。
但对于多进程的应用来说,由于多个进程之间会共享很多内容,包括代码、资源、so库等,因此单个进程结束造成的影响就会比较明显。
以有两个进程的应用为例,进程共享着部分内存,因此当一个进程不需要这些内存时,就会表现为一个进程的内存下降了,另一个进程的内存就会明显上升。
由此可见,在统计多进程的应用内存和进行优化时,需要综合考虑,以免出现努力优化了一个进程的内存,却造成其他进程内存增长的情况。
二、优化dex相关内存
上面提到,随着代码功能的增加,代码复杂度也在不断地变大,这时发现Dalvik Other和Dex Mmap这两部分消耗的内存也在不断地增加。
有时这两部分的内存已经接近总内存的一半。在Dalvik Heap已经充分优化的情况下,有必要研究这部分内存如何优化。
Dalvik Other存放的是类的数据结构及关系;Dex Mmap是类函数的代码和常量。通常要减少
这部分内存,需要从代码出发,精简无用代码,或者将功能插件化。
但如果我们深入理解了系统,也能够找到一些其他方法来降低这部分的内存消耗。
1、从clas对象说起
在MAT的对象实例列表中,往往能有很多class条目。这些对象是各种类型的元数据。从MAT的信息来看,它们只是保存了各个类的静态成员,所以对于没有静态成员的类型,Shallow Heap的值为0,并不消耗内存。
但实际上,这只是class消耗内存的冰山一角。
举例用数学处理库提供的函数新建两个对象,但将代码在一个空应用中执行后,发现Dalvik Heap、Dalvik Other、.dex mmap内存的增长;
Dalvik Heap的增长是我们预期的。通常来说,能够从代码的逻辑中分析出执行这段代码总共需要分配多少内存,也能够在MAT中看到新建对象消耗的内存。
当应用使用完新建的对象后,就会将heap内存释放,但Dalvik Other和.dex mmap部分是不会释放的。
接下来分析一下这两部分为什么消耗了这么多内存。
2、一个类的内存消耗
首先,如果代码中要使用一个类,例如以下代码:
Foo f = new Foo();
虚拟机执行这步时会做以下:
第一步时loadClass操作,将类消息从dex文件加载进内存:
(1)读取.dex mmap中classs对应的数据;
(2)分配native-heap和dalvik-heap内存创建class对象;
(3)分配dalvik-LinearAlloc存放class数据;
(4)分配dalvik-aux-structure存放class数据;
第二步时new instance操作,创建实例对象:
(1)执行.dex mmap中<clinit>和<init>代码;
(2)分配dalvik-heap创建class对象实例;
在这个过程中,可能还会分配dalvik-bitmap和jit-code-coach内存。
如果class Foo引用了其他类型,那就还需要先按照同样的逻辑创建被引用的class。
由此可见,在创建一个类实例的每一步都需要消耗内存。
3、dex mmap
dex mmap在Android应用中的作用是映射class.dex文件。Dalvik虚拟机需要从dex文件中加载类信息、字符串变量等,还需要
在调用函数的时候直接从mmap内存中读取函数代码(dvm bytecode)来执行;所以该部分内存是程序运行必不可少的。
以一个应用举例,在MAT中看到,应用加载了大约1500个class类型,而dex文件的class类型共有10635个。
使用dex mmap动态统计功能统计后发现,虽然只加载了1500个类,但dex内存通常高达4-6MB,差不多是dex文件大小的一半。
很大一部分dex内存空间被浪费了,实际使用到的数据和代码并没有那么多。
这是由于dex文件在生成时按字母顺序排列。由于4KB页面加载的原因,实际运行时会加载许多相邻但不会被用到的数据;
例如代码中使用了A1类,虚拟机就需要加载包含A1类数据的页面,但由于A1的数据只有1KB,那在加载的4KB页面中,还会有A2A3A4类,总共占用了4KB内存。
假设代码里用到A1类后,还会用到B1,C1,D1类,那么如果能在dex文件中将A1,B1,C1,D1类放在一起,虚拟机就只需要加载一个4KB页面,不仅减少了内存使用,还对程序的运行速度有好处。
因此优化的思路就是调整dex文件中数据的顺序,将能够用到的数据紧密排列到一起。
4、dex文件优化
为了节约空间,dex将原先在各个class文件中重复的信息集中放置在一起,并以索引和指针的形式支持快速访问。
虚拟机能够通过索引表在Data区域中找到需要的信息。
生成文件时要尽量将使用到的数据内容排布在一起。
在APK编译流程中,Proguard工具能够对类名进行修改,可以根据程序运行的逻辑,将那些会互相调用的类改为同一个package名,这样就可以是他们的数据排布在一起。
三、总结
介绍了Android应用的各种内存组成,以及这些成分是如何被消耗的。也总结出了一些节约和优化内存的经验;列出如下:
1、内存的主要组成索引
(1)Native Heap:Native代码分配的内存,虚拟机和Android框架本身也会分配;
(2)Dalvik Heap:Java代码分配的对象;
(3)Dalvik Other:类的数据结构和索引;
(4)so mmap:Native代码和常量;
(5)dex mmap:java代码和常量;
2、内存工具
(1)Android Studio/Memory Monitor:观察Dalvik内存;
(2)dumpsys meminfo:观察整体内存
(3)smaps:观察整体内存的详细组成;
(4)Eclipse Memory Analyzer:详细分析Dalvik内存;
3、测试经验
(1)MAT时探索Java堆并发现问题的好帮手,能够迅速发现常见的图片和大数组问题;
(2)仅靠MAT提供的功能也不是万能的,比如内存碎片问题就隐藏在对象的地址中;
(3)要测试非Dalvik部分,有必要了解Linux的进程和内存原理、内存共享机制,熟悉常用命令行工具;
(4)内存分配的最小单位时页面,通常是4KB,这个限制往往会引发各种碎片问题;
(5)碎片不仅仅是Dalvik内存,包括各种文件的mmap也有可能产生碎片;
4、性能优化
(1)尽量不要在循环中创建很多临时变量;
(2)可以将大型的循环拆散、分段或者按需执行;
(3)引入SDK库和调用新的系统API时需要考虑成本。有可能一些不常用的功能会导致大量的消耗。
这时候有可能需要多进程方案,将这些影响内存的操作放入临时进程执行。
(4)除了Dalvik堆内存,还有其他类型的内存在了解了原理后也能够进行分析和优化;
(5)dex文件有很多优化空间。在仔细统计并调整了dex文件的顺序后,往往能够节约1MB以上的mmap内存。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/157752.html