Android性能优化——内存优化

1.内存概念介绍

Art虚拟机内存分配与回收

Art堆划分:

  • Image Space 连续地址空间,不进行垃圾回收,存放系统预加载类,而这些对象是存放system@[email protected]@classes.oat这个OAT文件中的,每次开机启动只需把系统类映射到Image Space。
  • Zygote Space 连续地址空间,匿名共享内存,进行垃圾回收,管理Zygote进程在启动过程中预加载和创建的各种对象、资源。
  • Allocation Space 与Zygote Space性质一致,在Zygote进程fork第一个子进程之前,就会把Zygote Space一分为二,原来的已经被使用的那部分堆还叫Zygote Space,而未使用的那部分堆就叫Allocation Space。以后的对象都在Allocation Space上分配。
  • Large Object Space 离散地址空间,进行垃圾回收,用来分配一些大于12K的大对象。

注:Image Space和Zygote Space在Zygote进程和应用程序进程之间进行共享,而Allocation Space就每个进程都独立地拥有一份。

注意,虽然Image Space和Zygote Space都是在Zygote进程和应用程序进程之间进行共享,但是前者的对象只创建一次,而后者的对象需要在系统每次启动时根据运行情况都重新创建一遍。

当满足以下三个条件时,在large object heap上分配,否则在zygote或者allocation space上分配:

  • 请求分配的内存大于等于Heap类的成员变量large_object_threshold_指定的值。这个值等于3 * kPageSize,即3个页面的大小。

  • 已经从Zygote Space划分出Allocation Space,即Heap类的成员变量have_zygote_space_的值等于true。

  • 被分配的对象是一个原子类型数组,即byte数组、int数组和boolean数组等。

Art GC

与GC有关参数

  • [dalvik.vm.heapgrowthlimit]: [256m] 默认情况下, App可使用的Heap的最大值, 超过这个值就会产生OOM。

  • [dalvik.vm.heapsize]: [512m] 如果App的manifest配置了largeHeap属性, 则App可使用的Heap的最大值为此项设定值。

  • [dalvik.vm.heapstartsize]: [8m] App启动后, 系统分配给它的Heap初始大小. 随着App使用会增加。

  • [dalvik.vm.heapmaxfree]: [8m] GC后,堆最大空闲值

  • [dalvik.vm.heapminfree]: [512k] GC后,堆最小空闲值

  • [dalvik.vm.heaptargetutilization]:[0.75] GC后,堆目标利用率

虚拟机主流GC算法:

引用计数算法(jdk1.2之前):堆中的每个对象对应一个引用计数器,创建对象置为1,每次引用到此对象+1,其中一个引用销毁-1,变为0即满足回收。致命缺点:循环引用的对象无法进行回收。

可达性算法(jdk1.2之后):确定GC root,寻找路径可达的引用节点,形成可达性树,不在树上的节点即满足回收条件。

标记-清除算法(Mark-Sweep):遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。对没标记的对象全部清除。
优点:对不存活对象进行处理,在存活对象高的情况下非常高效。
缺点:清除对象不会整理,造成内存碎片,这部分内存碎片属于内部碎片。

复制算法(Coping): 遍历所有的GC Roots,将可达的对象复制到另一块内存空间,遍历完后清空原来的内存空间(剩下的都是不可达对象)。
优点:对可达对象进行复制,在存活的对象比较少时极为高效。
缺点:需要额外的内存空间。

标记-整理算法(Mark-Compact):在标记-清除算法基础上,增加存活对象内存整理。
优点:不造成内存碎片,也不需要额外内存空间
缺点:整理过程耗时,效率不高。

Art的三种GC策略

Sticky GC :只回收上一次GC到本次GC之间申请的内存。
Partial GC:局部垃圾回收,除了Image Space和Zygote Space空间以外的其他内存垃圾。
Full GC: 全局垃圾回收,除了Image Space之外的Space的内存垃圾。

策略的对比:(gc pause 时间越长,对应用的影响越大)

GC 暂停时间:Sticky GC < Partial GC < Full GC
回收垃圾的效率:Sticky GC > Partial GC > Full GC

Android内存模型与垃圾回收

2.内存问题分类

常规内存问题主要有三类:

内存溢出:

指程序在申请内存时,没有足够的内存空间供其使用,造成OOM。往往是由内存抖动、内存泄漏等问题量变到质变之后出现。

内存溢出伴随的是crash日志,它分两种情况:一种是大对象直接造成的OOM,比如bitmap,这种看报错日志就能定位到问题,比较好解决; 还有一种是存在内存泄漏,压死骆驼的最后一根稻草报的crash信息并不能准确反映出oom的真正问题。可以结合内存泄漏一起分析。

示意图

内存抖动:

短时间内有大量的对象被创建或者被回收。频繁创建和销毁对象造成频繁GC, 导致内存不足及碎片。

内存抖动主要就是循环或者频繁调用处短时间内创建和销毁大量对象,这里值得警惕的是频繁调用点的字符串”+”拼接的log日志,还有用ArrayList做大量的非尾部remove操作等。

示意图

内存泄漏:

指程序在申请内存后,无法释放已申请的内存空间。造成可用内存逐渐减少。

Android内存泄漏常见场景以及解决方案

1、资源性对象未关闭 对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap 等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。

2、注册对象未注销 例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。

3、类的静态变量持有大数据对象 尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。

4、单例造成的内存泄漏 优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封 装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。

5、非静态内部类的静态实例 该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源 不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如 果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置 空让GC可以回收,否则还是会内存泄漏。

6、Handler临时性内存泄漏 Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引 用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的, 则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息, 当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message 持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回 收,引发内存泄漏。

解决方案如下所示:

1、使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这 样在回收时,也可以回收Handler持有的对象。

2、在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中 有待处理的消息需要处理。 需要注意的是,AsyncTask内部也是Handler机制,同样存在内存泄漏风险,但其一般是临时性的。对于 类似AsyncTask或是线程造成的内存泄漏,我们也可以将AsyncTask和Runnable类独立出来或者使用静 态内部类。

7、容器中的对象没清理造成的内存泄漏 在退出程序之前,将集合里的东西clear,然后置为null,再退出程序

8、WebView WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为 WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业 务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

9、使用ListView时造成的内存泄漏 在构造Adapter时,使用缓存的convertView。

代码质量 & 数量

3.内存问题分析

分析工具

1)adb命令

对应用进程和系统整体内存状态做一个宏观把控。

具体分析参考之前文章:性能优化工具(十)- Android内存分析命令

2)Memory Profiler

操作应用程序过程中,以实时图表的形式反馈当前的内存情况,对像明显的内存抖动、内存泄漏能做一个初步分析。
具体使用参数之前文章:性能优化工具(十三)-使用 Memory Profiler 查看 Java 堆和内存分配

3)leakCanary

傻瓜式内存泄漏检测工具,对于Activity/Fragment的内存泄漏检测非常好用。
具体使用参考:

LeakCanary源码探讨

LeakCanary2 源码分析

LeakCanary,30分钟从入门到精通

4)MAT

内存问题全面分析工具,也可以说是兜底工具,使用相对复杂点。
具体使用参考之前文章:性能优化工具(八)-MAT

内存泄漏分析整体思路:

首先通过adb命令对整个内存状况做个宏观把握:
cat proc/meminfo

MemTotal:        2914764 kB
MemFree:           78008 kB 系统空闲内存(系统尚未被使用的,total-free = used)
MemAvailable:     440972 kB 可用内存(memfree + 可回收内存(部分buffer/cached,slab也能回收一部分))
...
SwapTotal:       1048572 kB 交换空间的总大小(设置的zram交换空间大小)
SwapFree:         471124 kB 未被使用交换空间的大小
...
Slab:             176044 kB 内核中slab分配的内存大小(slab = SReclaimable+SUnreclaim)
SReclaimable:      55528 kB 可收回Slab的内存大小
SUnreclaim:       120516 kB 不可收回Slab的内存大小

这里主要关注:
系统剩余内存MemAvailable,如果比较低代表当前系统内存整体不足;Zram开没开,SwapFree还剩余多少;Slab占用内存多大,其中SUnreclaim部分占用内存是多少,看是否有kernel泄漏。

dumpsys meminfo

Total PSS by process:         Java层存活的进程及其占用内存情况
   241,086K: system (pid 1479)
   161,423K: surfaceflinger (pid 544)
   137,754K: com.android.systemui (pid 4843 / activities)
   ...
Total PSS by OOM adjustment:  Native存活的进程及其占用内存情况
   376,783K: Native
       161,423K: surfaceflinger (pid 544)
        14,303K: audioserver (pid 725)
         9,247K: zygote (pid 719)
   ...
   576,007K: Persistent      按进程优先级分别来统计对应的进程及其内存使用情况
       241,086K: system (pid 1479)
   ...
   219,381K: Foreground
       167,657K: com.tengxin.youqianji (pid 29421 / activities)
   ...
   317,970K: B Services
        33,115K: com.UCMobile:channel (pid 25225)
   ...
   410,541K: Cached
        36,294K: com.android.vending (pid 13418)
   ...

这里主要看下当前存活的进程是否有占用内存明显异常的,另外看看不同优先级进程的比例如何,如果当前可以内存比较低,但是B services和cache类型进程数量还比较高,那得看framework内存管理策略是否有问题。

一般来说,内存比较低时,lmk会从cache开始杀进程,并且b services进程会降级为cache,变相增加lmk第一档查杀数量。

dumpsys meminfo pid

** MEMINFO in pid 1479 [system] **
                  Pss  Private  Private  SwapPss     Heap     Heap     Heap
                Total    Dirty    Clean    Dirty     Size    Alloc     Free
               ------   ------   ------   ------   ------   ------   ------
  Native Heap    26106    25984      104     8739    73728    34267    39460
  Dalvik Heap    63706    63676        4     1858    56528    40144    16384
  Dalvik Other     6704     6668       12       24
       Stack     2388     2364       24     1352
      Ashmem     8004     8000        0        0
     Gfx dev      532      124        0        0
   Other dev       55        8       36        0
    .so mmap     1561      444      780      488
   .jar mmap        0        0        0        8
   .apk mmap      145        0        0        0
   .dex mmap    28702        0     4428       40
   .oat mmap    78494        0    49992        0
   .art mmap     3068     2684       76      856
  Other mmap       34        4        0        0
   GL mtrack     1424     1424        0        0
     Unknown     1275     1084      188     2882

       TOTAL   238445   112464    55644    16247   130256    74411    55844

App Summary
                      Pss(KB)
                       ------
          Java Heap:    66436 从Java或Kotlin代码分配的对象内存。受dalvik.vm相关配置限制
        Native Heap:    25984 从C或C ++代码分配的对象内存。不受限
               Code:    55644 应用用于处理代码和资源(如dex字节码,已优化或已编译的dex码,.so库和字体)的内存。
              Stack:     2364 应用中的原生堆栈和Java堆栈使用的内存。这通常与您的应用运行多少线程有关。
           Graphics:     1548 图形缓冲区队列向屏幕显示像素所使用的内存。这是与CPU共享的内存,不是GPU专用内存。
      Private Other:    16132
             System:    70337
              TOTAL:   238445       TOTAL SWAP PSS:    16247

Objects
              Views:        3         ViewRootImpl:        1
        AppContexts:       20           Activities:        0 当前存活的activity
             Assets:        8        AssetManagers:        6
      Local Binders:      888        Proxy Binders:     1652
      Parcel memory:     1697         Parcel count:      751
   Death Recipients:      753      OpenSSL Sockets:        0

如果在dumpsys meminfo中发现明显内存异常的进程,那么直接dump对应的进程详细内存分配数据,看看是什么原因。Java、Native或者Graphics图形占据内存比较高,是否存在泄漏情况,另外关注Activities,比如以前就看到过由相机进入相册重复创建activity的情况,造成大量activity泄漏。

另外还可以关注下当前系统内存碎片情况:

cat /proc/pagetypeinfo

   Number of blocks   type       Unmovable      Movable     Reclaimable    CMA    HighAtomic    Isolate
      Node 0, zone       DMA         167          239             8         43          0         0
      Node 0, zone       Normal      228          266             11         0          1         0

Unmovable 超过总数的20%,可能存在内存碎片。

另外还可以dumpsys cpuinfo 关注下当前kswapd0 cpu占用率 ,侧面反映内存不足。

在有现场和有有效的bugreport信息的情况下,通过adb命令分析,基本对内存问题做一个基本面的判断,是否存在内存不足,如果内存不足是哪个层面的问题?

user space内存问题 还是kernel space内存问题,user space的话,看是系统进程内存问题,还是应用进程内存问题,同时也还能细分到是java heap的问题 还是native heap的问题。

那么总结下,最终问题会分为三类:java heap问题、native heap问题、kernel问题。

1)java内存泄漏分析

Android提供了hprof机制,通过MAT进行定位分析。

分析步骤:
首先如果是android studio抓取的hprof文件,需要做下转换才能在MAT上打开:hprof-conv 源文件路径 转换文件路径,然后通过MAT来分析转换后的hprof文件,MAT提供了若干分析视角:

histogram 基于类的角度
列举所有对象情况,可以group by class、package等切换视角。可以在里面具体检索某个类,对类可以看它当前的引用关系:outgoing 我引用了哪些类 ,incoming 哪些类引用了我 内存泄漏看这个。
基本数据字段说明:

  • object 对象数目

  • shallow 对象自己占多少内存

  • retained 在我这个引用链之上,对象总共占用多少内存

dominator_tree基于对象的角度
每个对象的支配数,percentage 是对象在所有对象中占的百分比。

top consumers 占用内存比较高的对象

leak suspects 内存泄漏怀疑点

OQL sql操作

2)native内存泄漏分析

Android提供了分析native进程内存泄漏的方法---malloc debug。

详细使用参考kernel文档:bionic/libc/malloc_debug/README.md

步骤:
1)adb root
2)adb shell setenforce 0
3)adb shell chmod 0777 /data/local/tmp
4)adb shell setprop libc.debug.malloc.program app_process64 //跟踪zygote及zygote的子进程
5)adb shell setprop libc.debug.malloc.options "backtrace_enable_on_signal leak_track"
6)adb shell stop
7)adb shell start
8)adb shell kill -45 <需要跟踪的进程号> //enable backtrace
9)adb shell kill -47 <需要跟踪的进程号> //在/data/locat/tmp/目录下会生成名为backtrace_heap..txt,logcat中也会打印出内存泄漏调用栈信息,如下图片所示

Android性能优化——内存优化_第1张图片

图片

当复现出问题时,可以通过几次抓取的log,对比找出一直malloc没有free的调用栈,即内存泄漏点。
10)使用native_heapdump_viewer.py解析backtrace_heap生成.html文件,分析内存占用情况。

以9.0的代码为例,调试audioserver进程

1)adb root
2) # setenforce 0  //避免由于selinux权限问题,无法生成heap文件
3) # setprop libc.debug.malloc.program audioserver // 调试audioserver进程
4) # setprop libc.debug.malloc.options "backtrace_enable_on_signal leak_track"
5) #kill -9 pid_ audioserver // 杀掉audioserver进程, 该进程会重启
logcat日志会有如下信息:
# logcat -v time | grep malloc_debug
09-26 20:19:41.573 I/malloc_debug( 6934): /system/bin/audioserver: Run: 'kill -45 6934' to enable backtracing.
09-26 20:19:41.573 I/malloc_debug( 6934): /system/bin/audioserver: Run: 'kill -47 6934' to dump the backtrace.
6) #kill -45 pid_ audioserver_new(6934) // 使能backtrace,然后复测内存泄漏问题,当audioserver进程存在内存泄漏时,执行下一步生成内存泄漏的backtrace
7) # kill -47 pid_ audioserver_new(6934)  // 在/data/local/tmp目录下面生成backtrace_heap文件
/data/local/tmp # ls -al
-rw------- 1 audioserver  audio       89704 2019-09-26 20:30 backtrace_heap.6934.txt
8)使用native_heapdump_viewer.py解析backtrace_heap.6934.txt
命令如下:
python development/scripts/native_heapdump_viewer.py --verbose --html backtrace_heap.6934.txt --symbols ./out/target/product/raphael/symbols > backtrace_heap.html

在调试native进程内存泄漏问题的时候要注意以下两个方面:

  • 需要setenforce 0,关闭selinux权限,否则无法在/data/local/tmp目录下面生成backtrace heap文件;解析backtrace heap文件

  • 用到native_heapdump_viewer.py这个脚本,9.0源码目录下面development/scripts/的这个脚本文件不能正常解析,需要使用10.0源码里面的这个文件,或者从下面这个地方去下载:https://android.googlesource.com/platform/development/+/master/scripts/native_heapdump_viewer.py

4.内存问题监控

浅谈 Android 内存监控(上)

抖音 Android 性能优化系列:Java 内存优化篇

内存快照裁剪

https://github.com/KwaiAppTeam/KOOM

你可能感兴趣的:(性能优化,内存优化)