编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 01.崩溃捕获设计实践
  • 02.崩溃治理优化总结
  • 03.Native崩溃治理实践
  • 04.ANR监控设计实践
  • 05.CPU消耗优化实践
  • 06.卡顿监控设计实践
  • 07.卡顿治理优化实践
  • 08.网络分析与优化实践
  • 09.线程优化实践操作
  • 10.高性能图片优化方案
  • 11.OOM异常优化实践
  • 12.内存监控优化方案
  • 13.内存治理优化实践
  • 14.FPS监测设计实践
  • 15.进程优化设计实践
  • 16.App启动优化实践
  • 17.App页面UI优化实践
  • 18.App稳定性专项实践
  • 19.App瘦身优化实践
  • 20.常见代码优化实践
  • 21.移动端防抓包实践
  • 22.App磁盘沙盒实践
  • 23.Ping工具开发实践
  • 24.Gradle构建优化实践
  • 25.CodeReview实践总结

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
贡献者: yangchong211
上一篇
11.OOM异常优化实践
下一篇
13.内存治理优化实践