文章目录
内存优化方向以及工具简介
内存问题主要体现在以下三个方面
内存抖动:内存占用图呈现锯齿状,容易导致GC频繁,加大卡顿的隐患
内存泄露:可用内存减少、频繁GC
内存溢出:OOM、程序异常
工具选择
Memory Profiler
-
实时图表展示应用内存使用情况
-
识别内存泄露、抖动(呈现锯齿状)
-
提供捕获堆转储(堆栈信息转为文件)、强制GC以及跟踪内存分配的能力
-
方便直观(除了实时展示内存使用情况,甚至还能查看对象创建的地方)
-
线下平时使用
Memory Analyzer (MAT)
- 强大的Java Heap分析工具,查找内存泄露以及内存占用
- 生成整体报告、分析问题等
- 线下使用
Java内存管理机制
Java内存分配
Java内存回收算法
标记-清除算法
-
标记处所有需要回收的对象
-
统一回收所有被标记的对象
缺点:
- 标记和清除效率不高,因为需要对每个对象进行扫描,标记处要回收的对象
- 产生大量不连续的内存碎片,不利于后续对象的内存分配
复制算法
- 将内存划分为大小相等的两块
- 一块内存用完之后复制存活的对象到另一块内存
- 对复制前的那一块进行清除
优点:当存活的对象比较少时,比较高效(是相对标记清除算法的,因为只需要对内存的一般扫描标记)
缺点:需要一块内存作为交换空间来进行对象的移动,或者说是浪费了一半空间,每次只用了一半的内存,代价大。
标记-整理算法
- 标记过程与“标记-清除”算法一样
- 存活的对象往一端进行移动
- 清除其余内存
避免了标记-清除算法导致的内存碎片,避免了复制算法的空间浪费,但是有对象的移动(更新指针),成本也高。
分代收集算法
- 结合多种收集算法优势
- 新生代对象存活率低,就采用复制算法
- 老年代对象存活率高,标记-整理算法
在虚拟机中,是多种算法相结合使用的,并不只是使用单一的算法。
Android内存管理机制
内存弹性分配:对Android设备来说,每打开一个App,并不是每个应用都给固定的内存,用的多也就给的多,是弹性分配的,分配值以及最大值受具体设备的影响,有些高端机给应用分配的内存可以达到500M,可能有的手机就只有100多M。
OOM场景:
- 内存真正不足,一种是达到了应用使用内存的上限阀值就OOM了
- 没有达到应用内存阀值,但是系统可用内存不足,再申请内存的时候就OOM了。
Dalvik与Art区别
- Dalvik仅固定一种回收算法
- Art(从5.0开始默认)回收算法可运行期选择
- Art具备内存整理能力,减少内存空洞
Low Memory Killer
进程分类(优先级):Android系统将尽量长时间地保持应用进程,但为了新进程或运行更重要的进程,需要清除旧进程来回收内存。为了确定保留或终止哪些进程,系统会对进程进行分类,需要时,系统会首先消除重要性最低的进程,然后是清除重要性稍低一级的进程,以此类推,以回收系统资源。下面按优先级从高到低,进行简单的介绍。
前台进程/foreground process
-
托管用户正在交互的Activity(已调用Activity的onResume()方法)
-
托管某个Service绑定到用户正在交互的Activity
-
托管正在“前台”运行的Service(服务已调用startForeground())
-
托管正在执行一个生命周期回调的Service(onCreate()、onStartCommand()、或onDestroy())
-
托管正执行其onReceiver()方法的BroadcastReceiver
可见进程/visible process
- Activity处于onPause, (还没有进入onStop())
- 绑定到前台Activity的Service(和前台进程的区别就是一个正在交互,一个仅仅可见)
服务进程/service process
- 通过 startService() 方法启动的进程
后台进程/background process
- 对用户没有直接影响的进程,Activity处于onStop的时候
空进程/empty process
- 不含任何活动应用组件的进程
- 保留这种进程的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间
- 为使总体系统资源咋进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程
当内存不足时,回收进程从优先级低的开始,同时回收收益,比如到底是回收30M还是300M的进程,也是一种参考因素。
内存抖动
基本介绍
- 定义:内存频繁分配和回收导致内存不稳定
- 表现:频繁GC、内存曲线呈锯齿状
- 危害:导致卡顿,严重时导致OOM
内存抖动导致OOM的原因分析
- 频繁创建对象,导致内存不足以及碎片(不连续)
- 不连续的内存片不易被分配,导致OOM
可通过Memory Profiler跟踪内存分配进行分析定位,内存呈现锯齿状,说明有内存抖动点击Record,随后stop,就可以查看堆栈信息,内存占用过多的对象,选中jump to source就可以定位到代码不规范的地方。此外还可以通过CPU Profiler结合代码进行排查,它可以记录定位哪一段代码消耗了时间,而我们内存抖动的代码一直在不断执行,结合起来是可以定位内存抖动的位置。内存抖动是由于频繁申请内存,频繁GC,所以出现内存抖动优先去找循环或者频繁调用的地方。
内存泄露
定义:垃圾对象无法被GC正常回收
表现:内存抖动、可用内存逐渐变少
危害:内存不足、GC频繁、OOM
关于内存泄露这一块想多说一点,到底什么是内存泄露呢?就是内存不在GC掌控之内了,原因就是GC垃圾回收机制对垃圾对象无法回收,从而导致垃圾对象持的内存持续占用,无法释放,造成可用内存减少,如果可用内存不断减少,就容易引发OOM。那么问题来了,GC判定一个对象是否可回收的依据是什么呢?如果一个对象被别的对象引用了,就不能被GC回收,对吗?答案是否定的,提一下强引用、软引用、弱引用、虚引用,你就知道了。在内存不足的时候,后面三种引用都可以被回收的。
常说的GC(Garbage Collector) Roots,特指的是垃圾收集器(Garbage Collector)的对象,GC Roots就是Java虚拟机中所有引用的根对象。我们都知道,垃圾回收器不会回收GC Roots以及那些被它们间接强引用的对象
一个对象可以属于多个root,GC Roots有以下几种:
- Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的Java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
- Thread - 活着的线程
- Stack Local - Java方法的local变量或参数
- JNI Local - JNI方法的local变量或参数
- JNI Global - 全局JNI引用
- Monitor Used - 用于同步的监控对象
- Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。
Memory Analyzer
下载链接:https://www.eclipse.org/mat/downloads.php
转换:hprof-conv 原文件路径 转换后文件路径
下面举个例子,具体说说MAT的使用,在此之前还有一种方式可以简单的判断是否发生了内存泄露,就是通过adb shell dumpsys meminfo packageName -d 命令查看内存情况,会显示许多信息,但是只要查看Objects部分即可:
在自己测试demo中,在MainActivity跳转到TestActivity,并调用finish方法,结束掉了MainActivity,但是Activities为2,再看前面的AppContexts为4,盲猜是Context的使用出问题了。这个命令只是一个初步的判断,详细的分析还是通过MAT工具,下面回到MAT的具体使用
首先Dump Java heap信息,生成并导出导出hprof文件
此时的hprof还不能立即使用,还需要进行转换:
可通过hprof-conv -z 原文件名 转换后文件名
接下来就通过MAT的File—>Open Heap dump 打开文件,点击Histogram分析内存泄露问题。为了便于查看,可进行按包名分组查看
根据包名找到相关对象列表,选中本该回收的对象MainActivity,查看外部引用
选中结果,在Path To GC Root 中,选中除去软,弱,虚引用
高潮来了,我们看到是instance,位置在AppSettings类中,
再去排查AppSettings.java 代码,发现了问题所在,在MainActivity通过AppSettings.getInstance(this) 初始化AppSettings时,但是静态的instance对象间接的持有了MainActivity的引用,长生命周期的对象持有短生命周期的对象,导致MainActivity对象不能被回收,
传参时将context改为context.getApplicationContext()即可。
context.getApplicationCotext() 即可解决这个问题。
public class AppSettings {
private static volatile AppSettings instance;
private static Context mContext;
public AppSettings(Context context) {
mContext = context;
}
public static AppSettings getInstance(Context context) {