安卓性能优化03-内存优化

性能优化03-内存优化

一、内存模型

Java内存模型:Java程序在运行时内存的模型。而Java代码是运行在Java虚拟机之上的,所以Java内存模型,也就是指Java虚拟机的运行时内存模型。

Java内存模型分为线程私有和共享数据区两大类,其中线程私有的数据区包含程序计数器PC、虚拟机栈、本地方法栈,所有线程共享的数据区包含Java堆、方法区,在方法区内有一个常量池。通常说的堆是指Java堆,栈是指虚拟机栈。

1.程序计数器PC

程序计数器PC是一块较小的内存空间,可以看作所执行字节码的行号指示器。字节码解释器就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,比如循环、跳转、异常处理等等这些基础功能都需要依赖这个计数器来完成。

当线程正在执行一个Java方法时,PC计数器记录的是正在执行的虚拟机字节码的地址;当线程正在执行的一个Native方法时,PC计数器则为空(Undefined)。

异常:不会抛出异常。

2.虚拟机栈

虚拟机栈描述的是java方法执行的内存模型,它的生命周期与线程相同。

每个方法(不包含native方法)执行的同时都会创建一个栈帧 用于存储局部变量表、操作数栈、动态链接、方法出口等信息。

Java虚拟机规范规定该区域有两种异常:

· StackOverFlowError:当线程请求栈深度超出虚拟机栈所允许的深度时抛出 (递归函数)

· OutOfMemoryError:当Java虚拟机动态扩展到无法申请足够内存时抛出 (OOM)

3.本地方法栈

本地方法栈和虚拟机栈差不多,前者是为虚拟机使用到的Native方法提供内存空间。有些虚拟机的实现直接把本地方法栈和虚拟机栈合二为一,比如主流的HotSpot虚拟机。

异常:Java虚拟机规范规定该区域可抛出StackOverFlowError和OutOfMemoryError。

4.Java堆

Java堆,是Java虚拟机管理的最大的一块内存,也是GC的主战场,所以可以叫它gc堆(垃圾堆),里面存放的是几乎所有的对象实例和数组数据。

异常:Java虚拟机规范规定该区域可抛出OutOfMemoryError。

5.方法区

方法区主要存放的是已被虚拟机加载的类信息、常量、静态变量、编译器编译后的代码等数据。

异常:Java虚拟机规范规定该区域可抛出OutOfMemoryError。

6.运行时常量池

运行时常量池是方法区的一部分,用于存放编译器生成的各种字面量和符号引用。运行时常量池除了编译期产生的Class文件的常量池,还可以在运行期间,将新的常量加入常量池,比较String类的intern()方法。

  1. 字面量:与Java语言层面的常量概念相近,包含文本字符串、声明为final的常量值等。
  2. 符号引用:编译语言层面的概念,包括以下3类:
    1. 类和接口的全限定名
    2. 字段的名称和描述符
    3. 方法的名称和描述符

异常:Java虚拟机规范规定该区域可抛出OutOfMemoryError。

7.变量

局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。

——因为它们属于方法中的变量,生命周期随方法而结束。

成员变量全部存储在堆中(包括基本数据类型,引用和引用的对象实体)

——因为它们属于类,类对象终究是要被new出来使用的。

二、垃圾回收

内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的。

1.垃圾判断机制

GC只会回收死去的对象,那怎么判断对象是否存活呢?

引用计数

引用计数是垃圾收集器中的早期策略。

引用计数:当一个对象被引用时,变量计数+1,不再引用时,变量计数-1。当变量技术为0时,就会被GC回收。

优点:引用计数收集器可以很快的执行,交织在程序运行中。对程序需要不被长时间打断的实时环境比较有利。

缺点:无法检测出循环引用。如父对象有一个对子对象的引用,子对象反过来引用父对象。这样,他们的引用计数永远不可能为0。

可达性分析

可达性分析:遍历所有的GC ROOT及其子节点,并添加标记。没有添加标记的对象就是无用的对象,可以被GC回收。

java中可作为GC Root的对象有

  1. 虚拟机栈(本地变量表)中正在运行使用的引用
  2. 方法区中静态属性引用的对象
  3. 方法区中常量引用的对象
  4. 本地方法栈JNI中引用的对象(Native对象)

回收流程

  1. GC第一次扫描,通过可达性算法分析对象是否GC Root强引用,即是否有用。
  2. 无用的对象会调用finalize()
  3. GC第二次扫描,调用finalize()后仍然无用的对象,会被GC回收。

2.垃圾回收算法

垃圾判定后就要进行回收,那怎么进行回收呢?

常用的垃圾回收算法:

  1. 标记-清除算法 Mark-Sweep
  2. 标记-整理算法 Mark-Compact
  3. 复制算法 Copying
  4. 分代收集算法,JVM、DVM使用的就是分代收集算法。

垃圾回收是内存抖动形成的原因,所以一个好的垃圾回收算法很重。

标记-清除算法

标记-清除算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收被标记的对象。

标记-清除算法不会进行对象的移动,直接回收不存活的对象,因此会造成内存碎片。内存碎片会导致内存不可用,从而造成OOM。

标记-整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

标记-压缩算法虽然缓解的内存碎片问题,但是它也引用了额外的开销,比如说额外的空间来保存迁移地址,需要遍历多次堆内存等。

复制算法

复制算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况。

分代收集算法

分代收集算法会根据内存的划分实现不同的收集算法。JVM还是DVM就是采用这种算法。

Java堆被分为新生代、老年代和永久代,新生代又被进一步划分为Eden和Survivor区, Survivor由From Space和To Space组成。

新生代

新建的对象首先分配到新生代的Eden区,当Eden满时,会把存活的对象转移到两个Survivor中的一个,当一个Survivor满了的时候会把不满足晋升的对象复制到另一个Survivor。

晋升的意思是对象每经历一次Minor GC (新生代中的gc),年龄+1,年龄达到设置的一个阀值后,被放入老年代。

两个Survivor的目的是避免碎片。如果只有一个Survivor,那Survivor被执行一次gc之后,可能对象是A+B+C。经历一次GC后B被回收。则会A| |C,造成碎片。

在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,所以一般选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。

老年代

用于存放新生代中经过N次垃圾回收仍然存活的对象。

老年代的垃圾回收称为Major GC。整堆包括新生代与老年代的垃圾回收称之为Full GC。

老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记-清除算法或标记-整理算法来进行回收。

永久代

主要存放所有已加载的类信息,方法信息,常量池等等。

该区域的对象不需要回收。

3.垃圾收集器

垃圾收集器是垃圾回收的执行者。

Serial串行收集器

Serial串行收集器是单线程的,使用复制算法回收垃圾。在进行垃圾收集时,必须暂停其他所有的工作线程,直到收集结束才能继续执行。

是client版本的java的默认新生代收集器。

优点:简单高效;

缺点:需要暂停线程;

ParNew 收集器

ParNew 收集器是多线程版的Serial串行收集器,使用复制算法。是server版本的虚拟机中首选的新生代收集器。

Parallel Scavenge收集器

Parallel Scavenge收集器相比ParNew 收集器,只有吞吐量不同:吞吐量更高。吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),决定CPU的利用率。

Serial Old收集器

Serial Old收集器是老年代版本的串行收集器,使用标记整理算法。

Parallel Old收集器

Serial Old收集器是老年代版本的并行收集器,使用标记整理算法。

CMS 收集器

Concurrent Mark Sweep收集器是一种以获得最短回收停顿事件为目标的收集器,也称为并发低停顿收集器或低延迟垃圾收集器,使用标记清除算法。

可以分为4个步骤:

  1. 初始标记(CMS initial mark)

    仅标记一下GC Roots能直接关联到的对象,速度很快,但需要”Stop The World”。

  2. 并发标记(CMS concurrent mark)

    进行GC Roots 追踪的过程,刚才产生的集合中标记出存活对象。

    由于应用程序也在运行,并不能保证可以标记出所有的存活对象;。

  3. 重新标记(CMS remark)

    为了修正并发标记期间因用户程序继续运作而导致标记变动的那一部分对象的标记记录;

    需要”Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短;

    采用多线程并行执行来提升效率;

  4. 并发清除(CMS concurrent sweep)

    回收所有的垃圾对象。

优点:并发收集低停顿。

缺点:

  1. 造成CPU资源紧张:会比其他收集器多开线程。
  2. 无法处理浮动垃圾:用户线程正在进行。
  3. 大量内存碎片:来源“标记—清除”算法。

G1收集器

Garbage-First收集器是当今收集器技术发展最前沿的成果之一,是一款面向服务端应用的垃圾收集器。

G1收集器和 CMS差不多,但是G1的采集范围是整个堆(新生代老生代)。他把内存堆分成多个大小相等的独立区域,在最后的筛选回收的时候根据这些区域的回收价值和成本决定是否回收掉内存。

Android的收集器

Dalvik虚拟机(4.4之前)主要使用标记清除算法,也可以选择使用拷贝算法。

ART(4.4以后) 有多个不同的 GC 方案,这些方案包括运行不同垃圾回收器。默认方案是 CMS。

三、内存泄漏

内存泄漏:不再使用的对象被GC Root持有强引用,导致GC无法回收。内存泄漏是针对堆内存。

内存分析工具:Android Profiler、MAT、LeakCanary。

1.Android Profiler

官方说明:https://developer.android.com/studio/preview/features/android-profiler.html

Android Profiler是AS3.0的分析工具。

使用:Android Profiler-Memory

  1. 查看内存使用情况
  2. 导出内存快照,使用AS打开
  3. 使用Analzer Tasks工具检测内存泄漏的Activity和重复的字符串。

2.MAT

下载地址:http://www.eclipse.org/mat/downloads.php

使用MAT之前,需要先转换快照文件,通过sdk/platform-tools/hprof-conv。

hprof-conv -z src dst //-z:排除不是app的内存

使用流程:

  1. 分析泄露Activity

    通过Merge Shortest Paths to GC Roots – exclue allphantom/weak/soft etc.references,可以查看持有强引用的GC Root对象,即造成内存泄漏的根源。

  2. 对比文件

    1. 打开两个内存快照,切换到直方图,点击Add to Compare Basket。
    2. 把视图切换到difference from base table,多出来的对象就是泄漏对象。
    3. 分析持有泄漏对象强引用的GC Root对象。
  3. InputMethodManager泄漏

    这是系统BUG。

    原因:Activity的DecorView请求了InputMethodManager,而InputMethodManager一直持有DecorView的引用,导致无法释放Activity。

    解决:在Activity的onDestory()中,通过反射,去除InputMethodManager对Activity的强引用。

3.LeakCanary

项目地址:https://github.com/square/leakcanary

LeakCanary可以自动检查内存泄漏。只需将LeakCanary集成到项目中,出现内存泄漏就会自动提醒。

优点:使用方便。

缺点:不够精准。

4.常见的内存泄漏

集合类

如果一个集合生命周期很长(如:静态变量),添加元素后没有删除,就可能造成内存泄漏。

静态成员

Static成员作为GC Root,如果持有外部对象的引用,那么这个外部对象不能被 GC 回收,导致内存泄露。

资源未关闭

BraodcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。

例如:

try{
  FileOutputStream fos=new FileOutputStream(file);
  fos.write("test".getBytes());
  fos.close();
}catch(Exception e){
  e.printStackTrace();
}
//上面的代码,如果出现异常,会导致fos未关闭。所以要在finally{}中close()。

非静态内部类

非静态内部类常见的有Handler和Thread。

如果使用非静态匿名内部类创建Handler,Hnadler就会持有Activity的强引用。当Activity退出时,持有Handler的Thread可能还在运行;Handler 发送的 Message 也可能未被处理,由于Message又持有Handler的引用。所以导致Handler无法回收,从而导致Activity无法被GC回收,导致内存泄漏。

对于Hnadler,有两种解决办法:

方法一:使用静态类创建Handler,传入Activity对象时,使用弱引用。

方法二:首先,在Activity退出时,关闭后台线程;然后从Message Qeque中移除Handler发出的Message。

mHandler.removeCallbacks();//方法一
mHandler.removeMessages();//方法二

Thread产生内存泄露的主要原因在于线程生命周期的不可控。

系统BUG

例如:InputMethodManager。

四、内存抖动

1.定义

内存抖动是指内存频繁地分配和回收,而频繁的gc会导致卡顿,严重时和内存泄漏一样会导致OOM。

内存抖动可以通过观察内存使用情况发现。

2.优化

尽量避免在循环体或者频繁调用的函数内创建对象,应该把对象创建移到循环体外。

五、内存回调函数

我们的Application、Acivity、Service、ContentProvider与Fragment都实现了ComponentCallbacks2接口。所以能够重写onTrimMemory与onLowMemory函数。

如果希望在其他组件中也能接收到这些回调可以使用上下文的registerComponentCallbacks注册接收,unRegisterComponentCallbacks反注册。

1.OnTrimMemory

OnTrimMemory的参数是一个int数值,代表不同的内存状态:

TRIM_MEMORY_RUNNING_CRITICAL:系统当前内存紧张,正在运行的进程应该释放非关键资源,来节省内存用于其他地方。如果内存得不到缓解,那么将会接到onLowMemory回调,并且后台进程会被杀死。

TRIM_MEMORY_RUNNING_LOW:系统当前内存较低,希望能释放一些不使用的资源,来节省内存用于其他地方

TRIM_MEMORY_RUNNING_MODERATE:系统当前内存情况一般,希望能释放一些不使用的资源,来节省内存用于其他地方。

TRIM_MEMORY_COMPLETE:该进程在后台进程列表最后一个,如果没有更多可用内存,马上就要被清理。

TRIM_MEMORY_MODERATE:该进程在后台进程列表的中部,释放更多的内存能帮助系统保持其他后台进程运行更久,提高系统整体性能。

TRIM_MEMORY_BACKGROUND:该进程进入LRU List,这是一个释放资源的好时机,能让我们的进程更久的存在内存,当用户返回app,能够快速构建,而不是重新创建启动。

TRIM_MEMORY_UI_HIDDEN:UI不可见了,应该释放占用大量内存的UI数据。比如说一个Bitmap,我们缓存下来是为了可能的(不一定)再次显示。但是如果接到这个回调,那么还是将它释放掉,如果回到前台,再显示会比较好。

2.OnLowMemory

OnLowMemory代表低内存,意味着后台进程已经被干掉了。这个回调可以作为4.0兼容OnTrimMemory的TRIM_MEMORY_COMPLETE来使用。

最后

性能优化专题:https://www.jianshu.com/nb/25128595

喜欢请点赞,谢谢!

    原文作者:最爱的火
    原文地址: https://www.jianshu.com/p/14046bba22a3
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞