Android性能优化系列二:内存优化

本文深入探讨Android内存优化,包括内存抖动和内存泄露。介绍了内存优化工具Memory Profiler和Memory Analyzer (MAT),讲解了Java和Android内存管理机制,分析内存抖动的原因及危害,并详细阐述了内存泄露的各种场景和解决方案,如避免静态内部类、Handler和Context引起的内存泄露问题。最后,提到了ARTHook在检测不合理图片方面的应用。
摘要由CSDN通过智能技术生成

内存优化方向以及工具简介

内存问题主要体现在以下三个方面

内存抖动:内存占用图呈现锯齿状,容易导致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) {
   
        
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值