首页 > 程序开发 > 移动开发 > Android > 正文
【Android 性能优化】—— 详解内存优化的来龙去脉
2017-03-20       个评论    来源:GuiM_007的博客  
收藏    我要投稿

Android 性能优化】—— 详解内存优化的来龙去脉。

APP内存的使用,是评价一款应用性能高低的一个重要指标。虽然现在智能手机的内存越来越大,但是一个好的应用应该将效率发挥到极致,精益求精。

1. 内存与内存分配策略概述 1.1 什么是内存

通常情况下我们说的内存是指手机的RAM,它主要包括一下几个部分:

- 寄存器(Registers读音:[?r?d??st?])

速度最快的存储场所,因为寄存器位于处理器内部,所以在程序中我们无法控制。

- 栈(Stack)

存放基本类型的对象和引用,但是对象本身不存放在栈中,而是存放在堆中。

变量其实是分为两部分的:一部分叫变量名,另外一部分叫变量值,对于局部变量(基本类型的变量和对象的引用变量)而言,统一都存放在栈中,但是变量值中存储的内容就有在一定差异了:Java中存在8大基本类型,他们的变量值中存放的就是具体的数值,而其他的类型都叫做引用类型(对象也是引用类型,你只要记住除了基本类型,都是引用类型)他们的变量值中存放的是他们在堆中的引用(内存地址)。

在函数执行的时候,函数内部的局部变量就会在栈上创建,函数执行结束的时候这些存储单元会被自动释放。栈内存分配运算内置于处理器的指令集中是一块连续的内存区域,效率很高,速度快,但是大小是操作系统预定好的所以分配的内存容量有限。

堆(Heap)

在堆上分配内存的过程称作 内存动态分配过程。在java中堆用于存放由new创建的对象和数组。堆中分配的内存,由java虚拟机自动垃圾回收器(GC)来管理(可见我们要进行的内存优化主要就是对堆内存进行优化)。堆是不连续的内存区域(因为系统是用链表来存储空闲内存地址,自然不是连续的),堆大小受限于计算机系统中有效的虚拟内存(32bit系统理论上是4G)

静态存储区/方法区(Static Field)

是指在固定的位置上存放应用程序运行时一直存在的数据,java在内存中专门划分了一个静态存储区域来管理一些特殊的数据变量如静态的数据变量。

常量池(Constant Pool)

顾名思义专门存放常量的。注意 String s = "java"中的“java”也是常量。JVM虚拟机为每个已经被转载的类型维护一个常量池。常量池就是该类型所有用到地常量的一个有序集合包括直接常量(基本类型,String)和对其他类型、字段和方法的符号引用。

总结:

定义一个局部变量的时候,java虚拟机就会在栈中为其分配内存空间,局部变量的基本数据类型和引用存储于栈中,引用的对象实体存储于堆中。因为它们属于方法中的变量,生命周期随方法而结束。成员变量全部存储与堆中(包括基本数据类型,引用和引用的对象实体),因为它们属于类,类对象终究是要被new出来使用的。当堆中对象的作用域结束的时候,这部分内存也不会立刻被回收,而是等待系统GC进行回收。所谓的内存分析,就是分析Heap中的内存状态。

1.2 Android中的沙盒机制

大家可能都听说过iOS中有沙盒机制(sandbox),但是我们的Android系统中也存在沙盒机制,只不过没有IOS中的严格,所以常常被人忽略。

由于Android是建立在Linux系统之上的,所以Android系统继承了Linux的 类Unix继承进程隔离机制与最小权限原则,并且在原有Linux的进程管理基础上对UID的使用做了改进,形成了Android应用的”沙箱“机制。

普通的Linux中启动的应用通常和登陆用户相关联,同一用户的UID相同。但是Android中给不同的应用都赋予了不同的UID,这样不同的应用将不能相互访问资源。对应用而言,这样会更加封闭,安全。

引文来自Android的SandBox(沙箱)

在Android系统中,应用(通常)都在一个独立的沙箱中运行,即每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik虚拟机实例。Dalvik经过优化,允许在有限的内存中同时高效地运行多个虚拟机的实例,并且每一个Dalvik应用作为一个独立的Linux进程执行。Android这种基于Linux的进程“沙箱”机制,是整个安全设计的基础之一。

引文来自浅析Android沙箱模型

简单点说就是在Android的世界中每一个应用相当与一个Linux中的用户,他们相互独立,不能相互共享与访问,(这也就解释了Android系统中为什么需要进程间通信),正是由于沙盒机制的存在最大程度的保护了应用之间的安全,但是也带来了每一个应用所分配的内存大小是有限制的问题。

2. Generational Heap Memory内存模型的概述

在Android和Java中都存在着一个Generational(读音:[?d?en??re???nl]) Heap Memory模型,系统会根据内存中不同的内存数据类型分别执行不同的GC操作。Generational Heap Memory模型主要由:Young Generation(新生代)、Old Generation(旧生代)、Permanent(读音:[?p?:rm?n?nt]) Generation三个区域组成,而且这三个区域存在明显的层级关系。所以此模型也可以成为三级Generation的内存模型。

Generational Heap Memory的模型

其中Young Generation区域存放的是最近被创建对象,此区域最大的特点就是创建的快,被销毁的也很快。当对象在Young Generation区域停留的时间到达一定程度的时候,它就会被移动到Old Generation区域中,同理,最后他将会被移动到Permanent Generation区域中。

三级内存模型的移动

在三级Generation内存模型中,每一个区域的大小都是有固定值的,当进入的对象总大小到达某一级内存区域阀值的时候就会触发GC机制,进行垃圾回收,腾出空间以便其他对象进入。

触发GC机制

不仅如此,不同级别的Generation区域GC是需要的时间也是不同的。同等对象数目下,Young Generation GC所需时间最短,Old Generation次之,Permanent Generation 需要的时间最长。当然GC执行的长短也和当前Generation区域中的对象数目有关。遍历查找20000个对象比起遍历50个对象自然是要慢很多的。

3. GC机制概述

与C++不用,在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但也随之带来了内存泄漏的可能。简单点说:对于 C++ 来说,内存泄漏就是new出来的对象没有 delete,俗称野指针;而对于 java 来说,就是 new 出来的 Object 放在 Heap 上无法被GC回收。

Android使用的主要开发语言是Java所以二者的GC机制原理也大同小异,所以我们只对于常见的JVM GC机制的分析,就能达到我们的目的。我还是先看看那二者的不同之处吧。

3.1 Dalvik 和标准Java虚拟机的区别 3.1.1 Dalvik 和标准Java虚拟机的主要区别

Dalvik虚拟机(DVM)是Android系统在java虚拟机(JVM)基础上优化得到的,DVM是基于寄存器的,而JVM是基于栈的,由于寄存器高效快速的特性,DVM的性能相比JVM更好。

3.1.2 Dalvik 和 java 字节码的区别

Dalvik执行.dex格式的字节码文件,JVM执行的是.class格式的字节码文件,Android程序在编译之后产生的.class 文件会被aapt工具处理生成R.class等文件,然后dx工具会把.class文件处理成.dex文件,最终资源文件和.dex文件等打包成.apk文件。

3.2 分别对Young Generation(新生代)和Old Generation(旧生代)采用的两种垃圾回收机制? 3.2.1 对于Young Generation(新生代)的GC

由于Young Generation通常存活的时间比较短,所以Young Generation采用了Copying算法进行回收,Copying算法就是扫描出存活的对象,并复制到一块新的空间中,这个过程就是下图Eden与Survivor Space之间的复制过程。Young Generation采用空闲指针的方式来控制GC触发,指针保存最后一个分配在Young Generation中分配空间地对象的位置。当有新的对象要分配内存空间的时候,就会主动检测空间是否足够,不够的情况下就出触发GC,当连续分配对象时,对象会逐渐从Eden移动到Survivor,最后移动到Old Generation。

Generational Heap Memory的模型
3.2.2 对于Old Generation(旧生代)的GC

Old Generation与Young Generation不同,对象存活的时间比较长,比较稳固,因此采用标记(Mark)算法来进行回收。所谓标记就是扫描出存活的对象,然后在回收未必标记的对象。回收后的剩余空间要么进行合并,要么标记出来便于下次进行分配,总之就是要减少内存碎片带来的效率损耗。

3.4 如何判断对象是否可以被回收

从上面的一小节中我们知道了不同的区域GC机制是有所不同的,那么这些垃圾是如何被发现的呢?下面我们就看一下两种常见的判断方法:引用计数、对象引用遍历。

3.4.1引用计数器

引用计数器是垃圾收集器中的早起策略。这种方法中,每个对象实体(不是它的引用)都有一个引用计数器。当一个对象创建的时候,且将该对象分配给一个每分配给一个变量,计数器就+1,当一个对象的某个引用超过了生命周期或者被设置一个新值时,对象计数器就-1,任何引用计数器为 0 的对象可以被当作垃圾收集。当一个对象被垃圾收集时,引用的任何对象技术 - 1。

优点:执行快,交织在程序运行中,对程序不被长时间打断的实时环境比较有利。

缺点:无法检测出循环引用。比如:对象A中有对象B的引用,而B中同时也有A的引用。

3.4.2 跟踪收集器

现在的垃圾回收机制已经不太使用引用计数器的方法判断是否可回收,而是使用跟踪收集器方法。

现在大多数JVM采用对象引用遍历机制从程序的主要运行对象(如静态对象/寄存器/栈上指向的堆内存对象等)开始检查引用链,去递归判断对象收否可达,如果不可达,则作为垃圾回收,当然在便利阶段,GC必须记住那些对象是可达的,以便删除不可到达的对象,这称为标记(marking)对象。

下一步,GC就要删除这些不可达的对象,在删除时未必标记的对象,释放它们的内存的过程叫做清除(sweeping),而这样会造成内存碎片化,布局已分配给新的对象,但是他们集合起来还很大。所以很多GC机制还要重新组织内存中的对象,并进行压缩,形成大块、可利用的空间。

为了达到这个目的,GC需要停止程序的其他活动,阻塞进程。这里我们要注意的是:不要频繁的引发GC,执行GC操作的时候,任何线程的任何操作都会需要暂停,等待GC操作完成之后,其他操作才能够继续运行, 故而如果程序频繁GC, 自然会导致界面卡顿. 通常来说,单个的GC并不会占用太多时间,但是大量不停的GC操作则会显著占用帧间隔时间(16ms。可参见《【Android 性能优化】—— UI篇》)。如果在帧间隔时间里面做了过多的GC操作,那么自然其他类似计算,渲染等操作的可用时间就变得少了。

4. Android内存泄漏分析
 
4.1 什么内存泄漏

对于 C++ 来说,内存泄漏就是new出来的对象没有 delete,俗称野指针;而对于 java 来说,就是 new 出来的 Object 放在 Heap 上无法被GC回收

 
4.2 为什么不能被回收

GC过程与对象的引用类型是严重相关的,下面我们就看看Java中(Android中存在差异)对于引用的四种分类:

- 强引用(Strong Reference):JVM宁愿抛出OOM,也不会让GC回收的对象

- 软引用(Soft Reference) :只有内存不足时,才会被GC回收。

- 弱引用(weak Reference):在GC时,一旦发现弱引用,立即回收

- 虚引用(Phantom Reference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

\

注意Android中存在的差异

但是在2.3以后版本中,系统会优先将SoftReference的对象提前回收掉, 即使内存够用,其他和Java中是一样的。所以谷歌官方建议用LruCache(least recentlly use 最少最近使用算法)。会将内存控制在一定的大小内, 超出最大值时会自动回收, 这个最大值开发者自己定。其实LruCache就是用了很多的HashMap,三百多行的代码

在开发过程中,保存对象,这时我很可以直接使用LruCache来代替,Bitmap对象:

在Android开发过程中,我们常常使用HasMap保存对象,但是为了防止内存泄漏,在保存内存占用较大、生命周期较长的对象的时候,尽量使用LruCache代替HasMap用于保存对象。

//指定最大缓存空间

private static final int MAX_SIZE = (int) (Runtime.getRuntime().maxMemory() / 8);

LruCache mBitmapLruCache = new LruCache<>(MAX_SIZE);

而造成不能回收的根本原因就是:堆内存中长生命周期的对象持有短生命周期对象的强/软引用,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收。

4.3 如何的监听系统发生GC

那么怎样才能去监听系统的GC过程呢?其实非常简单,系统每进行一次GC操作时,都会在LogCat中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式如下所示:

DVM中

D/dalvikvm(30615): GC FOR ALLOC freed 4442K, 25% free 20183K/26856K, paused 24ms , total 24ms

ART中

I/art(198): Explicit concurrent mark sweep GC freed 700(30KB) AllocSpace objects, 0(0B) LOS objects, 792% free, 18MB/21MB, paused 186us total 12.763ms

D/dalvikvm: , ,

原因,一般情况下一共有以下几种触发GC操作的原因:

GC_CONCURRENT: 当我们应用程序的堆内存快要满的时候,系统会自动触发GC操作来释放内存。GC_FOR_MALLOC: 当我们的应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行GC操作来释放内存。GC_HPROF_DUMP_HEAP: 当生成HPROF文件的时候,系统会进行GC操作,关于HPROF文件我们下面会讲到。GC_EXPLICIT: 这种情况就是我们刚才提到过的,主动通知系统去进行GC操作,比如调用System.gc()方法来通知系统。或者在DDMS中,通过工具按钮也是可以显式地告诉系统进行GC操作的。

点击复制链接 与好友分享!回本站首页
上一篇:Android 微信支付,授权,分享回调区分记录
下一篇:Android一起学Ui(1)----(折叠布局)
相关文章
图文推荐
文章
推荐
点击排行

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做实用的IT技术学习网站