12.内存监控优化方案
目录介绍
- 01.内存优化的概述
- 1.1 项目背景说明
- 1.2 遇到问题介绍
- 1.3 内存分析工具
- 1.4 设计目标
- 02.内存信息解读介绍
- 2.1 一些名次解释
- 2.2 查手机运行内存信息
- 2.3 获取Android Dalvik配置信息
- 2.4 获取指定Apk的内存信息
- 2.5 查看一个进程限制信息
- 2.6 查看进程状态信息
- 2.7 获取对象内存大小
- 03.方案基础设计
- 3.1 整体架构图
- 3.2 UML设计图
- 3.3 关键流程图
- 3.4 接口设计图
- 3.5 模块间依赖关系
- 04.一些技术要点说明
- 4.1
- 05.其他设计实践说明
- 5.1 性能设计
- 5.2 稳定性设计
- 5.3 灰度设计
- 5.4 降级设计
- 5.5 异常设计
01.内存优化的概述
1.1 项目背景说明
- 内存优化就是对内存问题的一个预防和解决,做内存优化能让应用挂得少、活得好和活得久。
- 挂得少 “挂” 指的是 Crash
- 导致 Android 应用 Crash 的原因有很多种,而做内存优化就能让我们的应用避免由内存问题引起的 Crash。 内存问题导致 Crash 的具体表现就是内存溢出异常 OOM,引起 OOM 的原因有多种。
- 活得好指的是使用流畅
- Android 中造成界面卡顿的原因有很多种,其中一种就是由内存问题引起的。 内存问题之所以会影响到界面流畅度,是因为垃圾回收(GC,Garbage Collection),在 GC 时,所有线程都要停止,包括主线程,当 GC 和绘制界面的操作同时触发时,绘制的执行就会被搁置,导致掉帧,也就是界面卡顿。
- 活得久指的是我们的应用在后台运行时不会被干掉。
- Android 会按照特定的机制清理进程,清理进程时优先会考虑清理后台进程。 清理进程的机制就是低杀。
- 用户在移动设备上使用应用的过程中被打断是很常见的,如果我们的应用不能活到用户回来的时候,要用户再次进行操作的体验就会很差。
1.2 遇到问题介绍
- 问题描述:
- OOM不知道是从哪里开始出现的,没办法确定具体引发OOM的地方
- 问题分析:
- 在OOM的时候如果去获取内存信息基本上是获取不到呢?原因是已经内存不足了,无法在创建数据
- 修改建议:
- 可以在临近OOM的时候提前获取一下内存的数据信息,来辅助问题定位
1.4 设计目标
- 一期方案的目的在于分析线上oom的问题根源,到底是什么原因产生的内存泄露,并且泄露的用户中占比有多大。
- 线上问题分析
- 建立内存数据等相关指标数据,对指标问题进行梳理和定义;建立端上内存数据采集SDK,统计相关数据
- 分析线上数据,特别是Android 5.0~Android 7.0 这个区间、Android8.0以上的用户,对Ram 大小信息进行分析归类
- 线下问题分析
- 建立so内存监控能力,分析so占用大小问题;对各种内存泄露问题case进行编写测试,复现相关场景
02.内存信息解读介绍
2.1 一些名次解释
2.1.1 基础概念说明
- VSS
- Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
- RSS
- Resident Set Size 实际使用物理内存(包含共享库占用的内存)
- PSS
- Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
- USS
- Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)
- 大小规律:
- 一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS
2.1.2 一些adb命令
- 应用启动后分配的初始内存:
- adb shell getprop|grep dalvik.vm.heapstartsize
- 查看单个应用程序最大内存限制的指令:
- adb shell getprop|grep heapgrowthlimit
- 单个 java 虚拟机最大的内存限制
- adb shell getprop|grep dalvik.vm.heapsize
- 上述查看到的单个内存最大限制为384MB,而meminfo里面dalvik heap size的最大值如果超过了384M就可能出现OOM。
- dalvik.vm.heapgrowthlimit和dalvik.vm.heapsize都是java虚拟机的最大内存限制
- 应用如果不想在dalvik heap达到heapgrowthlimit限制的时候出现OOM,需要在Manifest中的application标签中声明android:largeHeap=“true”,声明后,如果应用的dalvik heap达到heapsize的时候才会出现OOM!
- 另:设备不一样,最大内存的限制也可能不一样
2.2 查手机运行内存信息
- 使用adb
- adb shell cat /proc/meminfo
- 具体信息看image中图片:手机运行内存信息
- 下面先对"/proc/meminfo"文件里列出的字段进行粗略解释:
MemTotal 所有可用RAM大小 MemFree LowFree与HighFree的总和,被系统留着未使用的内存 Buffers 用来给文件做缓冲大小 Cached 被高速缓冲存储器(cache memory)用的内存的大小(等于diskcache minus SwapCache) SwapCached 被高速缓冲存储器(cache memory)用的交换空间的大小。已经被交换出来的内存,仍然被存放在swapfile中,用来在需要的时候很快的被替换而不需要再次打开I/O端口。 Active 在活跃使用中的缓冲或高速缓冲存储器页面文件的大小,除非非常必要,否则不会被移作他用。 Inactive 在不经常使用中的缓冲或高速缓冲存储器页面文件的大小,可能被用于其他途径。 SwapTotal 交换空间的总大小。 SwapFree 未被使用交换空间的大小。 Dirty 等待被写回到磁盘的内存大小。 Writeback 正在被写回到磁盘的内存大小。 AnonPages 未映射页的内存大小。 Mapped 设备和文件等映射的大小。 Slab 内核数据结构缓存的大小,可以减少申请和释放内存带来的消耗。 SReclaimable :可收回Slab的大小。 SUnreclaim 不可收回Slab的大小(SUnreclaim+SReclaimable=Slab)。 PageTables 管理内存分页页面的索引表的大小 NFS_Unstable 不稳定页表的大小。
2.3 获取Android Dalvik配置信息
- adb指令
- adb shell getprop|grep vm
- 下面先对"adb shell getprop|grep vm" 文件里列出的字段进行粗略解释:
dalvik.vm.checkjni 是否要执行扩展的JNI检查,CheckJNI是一种添加额外JNI检查的模式;出于性能考虑,这些选项在默认情况下并不会启用。此类检查将捕获一些可能导致堆损坏的错误,例如使用无效/过时的局部和全局引用。如果这个值为false,则读取ro.kernel.android.checkjni的值 ro.kernel.android.checkjni 只读属性,是否要执行扩展的JNI检查。当dalvik.vm.checkjni为false,此值才生效 dalvik.vm.execution-mode Dalvik虚拟机的执行模式,即:所使用的解释器,下文会讲解 dalvik.vm.stack-trace-file 指定堆栈跟踪文件路径 dalvik.vm.check-dex-sum 是否要检查dex文件的校验和 log.redirect-stdio 是否将stdout/stderr转换成log消息 dalvik.vm.enableassertions 是否启用断言 dalvik.vm.jniopts JNI可选配置 dalvik.vm.heapstartsize 堆的起始大小 dalvik.vm.heapsize 堆的大小 dalvik.vm.jit.codecachesize JIT代码缓存大小 dalvik.vm.heapgrowthlimit 堆增长的限制 dalvik.vm.heapminfree 堆的最小剩余空间 dalvik.vm.heapmaxfree 堆的最大剩余空间 dalvik.vm.heaptargetutilization 理想的堆内存利用率,其取值位于0与1之间 ro.config.low_ram 该设备是否是低内存设备 dalvik.vm.dexopt-flags 是否要启用dexopt特性,例如字节码校验以及为精确GC计算寄存器映射 dalvik.vm.lockprof.threshold 控制Dalvik虚拟机调试记录程序内部锁资源争夺的阈值 dalvik.vm.jit.op 对于指定的操作码强制使用解释模式 dalvik.vm.jit.method 对于指定的方法强制使用解释模式 dalvik.vm.extra-opts 其他选项
2.4 获取指定Apk的内存信息
- adb指令
- adb shell dumpsys meminfo org.yczbj.ycvideoplayer
- 注意:org.yczbj.ycvideoplayer需要换成你需要的apk包名
- 如下所示,内存信息
- 一般情况下我们只需要关心 PSS Total + Private Dirty
didi1@DIDI-C02F31XVML7H ~ % adb shell dumpsys meminfo org.yczbj.ycvideoplayer Applications Memory Usage (in Kilobytes): Uptime: 244969974 Realtime: 284504287 ** MEMINFO in pid 26970 [org.yczbj.ycvideoplayer] ** Pss Private Private SwapPss Rss Heap Heap Heap Total Dirty Clean Dirty Total Size Alloc Free ------ ------ ------ ------ ------ ------ ------ ------ Native Heap 48414 48344 0 88 49336 58300 21743 36556 Dalvik Heap 2191 2008 80 238 2948 3560 2670 890 Dalvik Other 1507 904 60 3 2332 Stack 612 612 0 0 620 Ashmem 2 0 0 0 16 Other dev 28 0 28 0 360 .so mmap 12733 360 4204 94 47200 .jar mmap 1138 0 4 0 34968 .apk mmap 410 0 0 0 12908 .ttf mmap 33 0 0 0 376 .dex mmap 5688 4 5680 0 5836 .oat mmap 309 0 0 0 3756 .art mmap 11974 9332 1540 403 22552 Other mmap 754 8 76 0 5972 Unknown 803 776 4 6 1056 TOTAL 87428 62348 11676 832 190236 61860 24413 37446 App Summary Pss(KB) Rss(KB) ------ ------ Java Heap: 12880 25500 Native Heap: 48344 49336 Code: 10252 106144 Stack: 612 620 Graphics: 0 0 Private Other: 1936 System: 13404 Unknown: 8636 TOTAL PSS: 87428 TOTAL RSS: 190236 TOTAL SWAP PSS: 832 Objects Views: 42 ViewRootImpl: 1 AppContexts: 5 Activities: 1 Assets: 14 AssetManagers: 0 Local Binders: 19 Proxy Binders: 44 Parcel memory: 6 Parcel count: 24 Death Recipients: 0 OpenSSL Sockets: 0 WebViews: 0 SQL MEMORY_USED: 0 PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0
2.5 查看一个进程限制信息
- adb指令
- adb shell cat /proc/29239/limits
didi1@DIDI-C02F31XVML7H ~ % adb shell cat /proc/29239/limits Limit Soft Limit Hard Limit Units Max cpu time unlimited unlimited seconds Max file size unlimited unlimited bytes Max data size unlimited unlimited bytes Max stack size 8388608 unlimited bytes Max core file size 0 unlimited bytes Max resident set unlimited unlimited bytes Max processes 27243 27243 processes Max open files 32768 32768 files Max locked memory 67108864 67108864 bytes Max address space unlimited unlimited bytes Max file locks unlimited unlimited locks Max pending signals 27243 27243 signals Max msgqueue size 819200 819200 bytes Max nice priority 40 40 Max realtime priority 0 0 Max realtime timeout unlimited unlimited us didi1@DIDI-C02F31XVML7H ~ %
2.6 查看进程状态信息
- adb指令
- adb shell cat /proc/29239/status
- 信息如下
didi1@DIDI-C02F31XVML7H ~ % adb shell cat /proc/29239/status Name: Thread-6 Umask: 0077 State: S (sleeping) Tgid: 29178 Ngid: 0 Pid: 29239 PPid: 535 TracerPid: 0 Uid: 10203 10203 10203 10203 Gid: 10203 10203 10203 10203 FDSize: 128 Groups: 1079 3003 3007 9997 20203 50203 NStgid: 29178 NSpid: 29239 NSpgid: 535 NSsid: 0 VmPeak: 6313184 kB VmSize: 6064200 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 159324 kB VmRSS: 156216 kB RssAnon: 51380 kB RssFile: 104316 kB RssShmem: 520 kB VmData: 1316328 kB VmStk: 8192 kB VmExe: 28 kB VmLib: 167296 kB VmPTE: 1164 kB VmSwap: 22900 kB CoreDumping: 0 Threads: 58 SigQ: 0/27243 SigPnd: 0000000000000000 ShdPnd: 0000000000000000 SigBlk: 0000000088001a04 SigIgn: 0000002000000001 SigCgt: 0000004e400084f8 CapInh: 0000000000000000 CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: 0000000000000000 CapAmb: 0000000000000000 NoNewPrivs: 0 Seccomp: 2 Speculation_Store_Bypass: unknown Cpus_allowed: 07 Cpus_allowed_list: 0-2 Mems_allowed: 1 Mems_allowed_list: 0 voluntary_ctxt_switches: 12 nonvoluntary_ctxt_switches: 1 didi1@DIDI-C02F31XVML7H ~ %
2.7 获取对象内存大小
- 1.什么时候要知道对象内存大小
- 在内存足够用的情况下我们是不需要考虑java中一个对象所占内存大小的。
- 但当一个系统的内存有限,或者某块程序代码允许使用的内存大小有限制,又或者设计一个缓存机制,当存储对象内存超过固定值之后写入磁盘做持久化等等,总之我们希望像写C一样,java也能有方法实现获取对象占用内存的大小。
- 2.在回答这个问题之前,我们需要先了解java的基础数据类型所占内存大小
- 1k = 1024个字节(byte);1M = 1024kb
数据类型 所占空间(byte) byte 1 short 2 int 4 long 8 float 4 double 8 char 2 boolean 1
- 当然,java作为一种面向对象的语言,更多的情况需要考虑对象的内存布局,java对于对象所占内存大小需要分两种情况考虑:
- 对象类型:一般非数组对象
- 内存布局构成:8个字节对象头(mark) + 4/8字节对象指针 + 数据区 + padding内存对齐(按照8的倍数对齐)
- 对象类型:数组对象
- 内存布局构成:8个字节对象头(mark) + 4/8字节对象指针 + 4字节数组长度 + 数据区 + padding内存对齐(按照8的倍数对齐)
- 对象类型:一般非数组对象
- 可以看到数组类型对象和普通对象的区别仅在于4字节数组长度的存储区间。而对象指针究竟是4字节还是8字节要看是否开启指针压缩。
- Oracle JDK从6 update 23开始在64位系统上会默认开启压缩指针:http://rednaxelafx.iteye.com/blog/1010079。
- 如果要强行关闭指针压缩使用-XX:-UseCompressedOops,强行启用指针压缩使用: -XX:+UseCompressedOops。
- 3.接下来我们来举例来看实现java获取对象所占内存大小的方法,假设我们有一个类的定义如下
private static class ObjectA { String str; // 4 int i1; // 4 byte b1; // 1 byte b2; // 1 int i2; // 4 ObjectB obj; // 4 byte b3; // 1 } ObjectA obj = new ObjectA();
- 如果我们直接按照上面掌握的java对象内存布局进行计算,则有:
- Size(ObjectA) = Size(对象头(_mark)) + size(oop指针) + size(数据区)
- Size(ObjectA) = 8 + 4 + 4(String) + 4(int) + 1(byte) + 1(byte) + 2(padding) + 4(int) + 4(ObjectB指针) + 1(byte) + 7(padding)
- Size(ObjectA) = 40
- 4.使用Unsafe来获取内存大小,代码如下所示
private final static Unsafe UNSAFE; // 只能通过反射获取Unsafe对象的实例 static { try { UNSAFE = (Unsafe) Unsafe.class.getDeclaredField("theUnsafe").get(null); } catch (Exception e) { throw new Error(); } } Field[] fields = ObjectA.class.getDeclaredFields(); for (Field field : fields) { System.out.println(field.getName() + "---offSet:" + UNSAFE.objectFieldOffset(field)); }
- 输出结果为:
str---offSet:24 i1---offSet:12 b1---offSet:20 b2---offSet:21 i2---offSet:16 obj---offSet:28 b3---offSet:22
- 我们同样可以算得对象实际占用的内存大小:
- Size(ObjectA) = Size(对象头(_mark)) + size(oop指针) + size(排序后数据区) = 8 + 4 + (28+4-12) = 32.
- 再回过头来,看我们在通过代码获取对象所占内存大小之前的预估值40。比我们实际算出来的值多了8个字节。
- 通过Unsafe打印的详细信息,我们不难想到这其实是由hotspot创建对象时的排序决定的:
- HotSpot创建的对象的字段会先按照给定顺序排列,默认的顺序为:从长到短排列,引用排最后: long/double –> int/float –> short/char –> byte/boolean –> Reference。
- 所以我们重新计算对象所占内存大小得:
- Size(ObjectA) = Size(对象头(_mark)) + size(oop指针) + size(排序后数据区)
- Size(ObjectA) = 8 + 4 + 4(int) + 4(int) + byte(1) + byte(1) + 2(padding) + 4(String) + 4(ObjectB指针)
- Size(ObjectA) = 32
- 与上面计算结果一致。
参考链接
- https://www.cnblogs.com/tesla-turing/p/11487815.html
- https://www.cnblogs.com/Kidezyq/p/8030098.html
- Android内存优化分析总结,这一篇就够了!
- https://juejin.cn/post/7123452813656457253
- Android内存优化的思考
- https://juejin.cn/post/7106289745499521054