安卓性能优化之内存优化
内存优化
- Android内存优化参考
- JsonChao内存优化总结
- 常用工具:leakcanary、MAT
- 大图检测:使用EPIC hook 图片加载;ASM对图片加载入口进行代码插桩
基础概念
-
几种内存问题的区别?
- 内存抖动:在短时间内反复地发生内存增长和回收,导致程序卡顿甚至OOM内存溢出
- 内存泄漏:一些内存对象无法按预期的释放
- 内存溢出:申请分配内存时,超过系统所能提供的
-
OOM的几种类型
- java.lang.StackOverflowError 栈溢出:递归、死循环等,因为每次方法调用都会有一个方法栈压入导致OOM
- java.lang.OutOfMemoryError:Java heap space:创建了非常多的对象实例/存储了过大的数据导致无法在新建对象
- java.lang.OutOfMemoryError:GC overhead limit exceeded:频繁GC,并且回收的内存空间很少,导致死循环(超过98%的时间用来做GC并且回收了不到2%的堆内存)
- java.lang.OutOfMemoryError:unable to create new native thread:创建的线程太多了,一般默认一个进程可以创建的线程数为1024个
- java.lang.OutOfMemoryError:Direct buffer memory:本地内存满了,一般是NIO中ByteBuffer.allocateDirect()分配的内存,这个内存不属于GC管辖可能不会被及时回收
-
java堆和native堆的区别
- java堆主要是Java代码分配的对象
- native堆主要是C代码(malloc)分配的内存
- 一些Java代码也会造成native堆内存分配,比如bitmap
-
虚拟内存
- 作为内存地址的抽象,提供一个映射到真实的内存地址,使每个进程认为自己拥有连续的内存地址,提高内存使用的效率
- 内存包含和隔离:每个进程只能访问到自己的内存地址(避免直接操作物理内存)
- 分页与置换:物理内存与虚拟内存都是以页为单位进行管理(一般是4K或8K),以页为单位的管理使内存管理更加高效
-
内存优化常见思路
- 对象复用/享元模式
- 减少不必要的内存分配
- 监测是否内存泄漏、是否因为生产者消费者模型未及时处理导致内容堆积
- 使用合理的数据结构,例如SparseArray、ArrayMap 来替代 HashMap
-
内存引用的几种类型
- 强引用:在内存不足时不会被回收。平常用的最多的对象,如新创建的对象。
- 软引用:在内存不足时会被回收(无强引用)。用于实现内存敏感的高速缓存。
- 弱引用:只要GC回收器发现了它,就会将之回收(无强引用)。用于Map数据结构中,引用占用内存空间较大的对象。
- 虚引用:在任何时候都可能被垃圾回收器回收(无强引用)。
常见内存泄漏
- Handler(持有外部引用)
- 单例泄漏(持有Activity而不是Application的Context)
- 匿名线程(持有外部引用)(但是Lambda表达式没有隐式持有外部类,但是显示持有的会泄漏)
- 非静态内部类创建出的静态实例对象(持有外部引用)
- Webview泄漏(渲染页面产生的堆内存,可以单独进程,退出杀进程解决)
- IO流、Bitmap等未释放、销毁(FD句柄未释放)
- native内存泄漏
- 全局Manager持有匿名监听器(可以提供反注册解决)
- 匿名内部类/lambda/Kotlin高阶函数 持有外部类并且执行耗时任务时,可能存在内存泄漏的风险
Java 内存划分
堆、程序计数器、方法区、本地方法栈、虚拟机栈,其中方法区和堆是线程共享的
- 各内存划分简介
-
方法区:线程共享,存储类信息、静态变量、常量、即时编译出来的代码数据,可造成OOM
-
堆:线程共享,存放几乎所有的对象实例,GC的主要区域
-
程序计数器
- 一块较小的内存空间,线程私有,存储当前线程执行的字节码行号指示器
- 字节码解释器通过改变这个计数器的值来选取下一条需要执行的字节码指令:分支、循环、跳转等
- 每个线程都有一个独立的程序计数器
- 唯一一个在java虚拟机中不会OOM的区域
-
本地方法栈
- 为虚拟机中Native方法服务,对本地方法栈中使用的语言、数据结构、使用方式没有强制规定,虚 拟机可自有实现
- 占用的内存区大小是不固定的,可根据需要动态扩展
-
虚拟机栈
- 线程私有区域,每个java方法在执行的时候会创建一个栈帧用于存储局部变量表、操作数栈、动态 链接、方法出口等信息。方法从执行开始到结束过程就是栈帧在虚拟机栈中入栈出栈过程
- 局部变量表存放编译期可知的基本数据类型、对象引用、returnAddress类型。所需的内存空间会 在编译期间完成分配,进入一个方法时在帧中局部变量表的空间是完全确定的,不需要运行时改变
- 若线程申请的栈深度大于虚拟机允许的最大深度,会抛出SatckOverFlowError错误
- 虚拟机动态扩展时,若无法申请到足够内存,会抛出OutOfMemoryError错误
对象存活判断算法
- 引用计数法
- 给对象添加引用计数器,每当一个地方引用时,计数器加1,引用失效时计数器减1;当引用计数 器为0时即为对象不可用
- 实现简单,效率高,但是无法解决相互引用问题,主流虚拟机一般不使用此方法判断对象是否存活
- 可达性分析法
- 以GC Roots作为起点,向下搜索,搜索走过的路径称为引用链,当一个对象到 GC Roots没有任何引用链时即为对象不可用,可被回收的
- 可被称为GC Roots的对象:虚拟机栈中引用的对象、方法区中类静态属性引用的对象、方法区中 常量引用的对象、本地方法栈中引用的对象
- GC Root对象
- Class-由系统ClassLoader加载的对象
- Thread-活着的线程
- Stack Local-Java方法的local变量或参数
- JNI Local - JNI方法的local变量或参数
- JNI Global - 全局JNI引用
- Monitor Used - 用于同步的监控对象
垃圾回收算法
- 标记清除算法
- 首先标记出需要回收的对象,在标记完成后统一回收所有标记的对象
- 缺点:
- 标记和清除的过程效率低
- 会产生很多不连续的内存碎片,申请大内存时容易失败而触发GC
- 标记整理算法
- 标记整理算法标记过程和标记清除算法一样,但清除过程并不是对可回收对象直接清理,而是将所有存 活对象像一端移动,然后集中清理到端边界以外的内存。
- 复制算法
- 将可用内存按空间分为大小相同的两小块,每次只使用其中的一块,等这块内存使用完了将还存活的对 象复制到另一块内存上,然后将这块内存区域对象整体清除掉。每次对整个半区进行内存回收,不会导 致碎片问题,实现简单高效。
- 缺点:内存可用空间减半
- 分代收集算法
- 根据对象存活周期的不同将内存划分为 新生代 和 老年代,根据其特点采用最合适的算法
- 新生代存活对象较少,每次垃圾回收都有大量对象死去,一般采用复制算法,只需要付出复制少量 存活对象的成本就可以实现垃圾回收
- 老年代存活对象较多,没有额外空间进行分配担保,就必须采用标记清除算法和标记整理算法进行 回收
Android Studio Profiler
- AS自带的Profiler也包括内存分析,根据业务场景,当发现有明显的内存波动时,导出Hprof文件,用MAT进行分析;
- 可以根据实际业务查看内存的分配找到相关的地方

各项指标实时获取
线程数量
合理的线程使用可提高应用程序的运行效率,过度使用反而会增加CPU及内存的负担。为避免这一情况的发生,可结合进程状态及当前的线程列表进行分析:
-
当前状态:读取进程状态 /proc/pid/status,并解释Threads字段
-
具体分析:调用Thread.getAllStackTraces() 获取当前所有线程的信息,包括线程名、调用栈及状态等
adb获取内存信息
adb shell dumpsys meminfo <package_name>字段 含义 备注 Shallow Size Shallow Size是指实例自身占用的内存, 可以理解为保存该’数据结构’需要多少内存, 注意不包括它引用的其他实例 Retained Size 实例A的Retained Size是指, 当实例A被回收时, 可以同时被回收的实例的Shallow Size之和 缩写 VSS Virtual Set Size 虚拟耗用内存(包含共享库占用的内存 RSS Resident Set Size 实际使用物理内存(包含共享库占用的内存 PSS Proportional set size 实际使用的物理内存(比例分配共享库占用的内存 USS Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存) 其他 VSS>=RSS>=PSS>=USS

最低0.47元/天 解锁文章
2万+

被折叠的 条评论
为什么被折叠?



