Android-内存优化

Android内存优化

1、ART、DVM、JVM的区别

DVM之所以不是一个JVM ,主要原因是DVM并没有遵循JVM规范来实现。DVM与JVM主要有以下区别。

1.基于的架构不同 
    JVM基于栈则意味着需要去栈中读写数据,所需的指令会更多,这样会导致速度慢,对于性能有限的移动设备,显然不是很适合。 
DVM是基于寄存器的,它没有基于栈的虚拟机在拷贝数据而使用的大量的出入栈指令,同时指令更紧凑更简洁。但是由于显示指定了操作数,所以基于寄存器的指令会比基于栈的指令要大,但是由于指令数量的减少,总的代码数不会增加多少。
2.执行的字节码不同 
    在Java SE程序中,Java类会被编译成一个或多个.class文件,打包成jar文件,而后JVM会通过相应的.class文件和jar文件获取相应的字节码。执行顺序为: .java文件 -> .class文件 -> .jar文件 
    而DVM会用dx工具将所有的.class文件转换为一个.dex文件,然后DVM会从该.dex文件读取指令和数据。执行顺序为: .java文件 –>.class文件-> .dex文件 

DVM允许在有限的内存中同时运行多个进程 

    DVM经过优化,允许在有限的内存中同时运行多个进程。在Android中的每一个应用都运行在一个DVM实例中,每一个DVM实例都运行在一个独立的进程空间。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。

DVM架构

    DVM的源码位于dalvik/目录下,其中dalvik/vm目录下的内容是DVM的具体实现部分,它会被编译成libdvm.so;dalvik/libdex会被编译成libdex.a静态库,作为dex工具使用;dalvik/dexdump是.dex文件的反编译工具;DVM的可执行程序位于dalvik/dalvikvm中,将会被编译成dalvikvm可执行程序。

DVM的运行时堆
    DVM的运行时堆主要由两个Space以及多个辅助数据结构组成,两个Space分别是Zygote Space(Zygote Heap)和Allocation Space(Active Heap)。Zygote Space用来管理Zygote进程在启动过程中预加载和创建的各种对象,Zygote Space中不会触发GC,所有进程都共享该区域,比如系统资源。Allocation Space是在Zygote进程fork第一个子进程之前创建的,它是一种私有进程,Zygote进程和fock的子进程在Allocation Space上进行对象分配和释放。 
除了这两个Space,还包含以下数据结构:
    Card Table:用于DVM Concurrent GC,当第一次进行垃圾标记后,记录垃圾信息。
    Heap Bitmap:有两个Heap Bitmap,一个用来记录上次GC存活的对象,另一个用来记录这次GC存活的对象。
    Mark Stack:DVM的运行时堆使用标记-清除(Mark-Sweep)算法进行GC

ART的运行时堆

与DVM的GC不同的是,ART的GC类型有多种,主要分为Mark-Sweep GC和Compacting GC。ART的运行时堆的空间根据不同的GC类型也有着不同的划分,如果采用的是Mark-Sweep GC,运行时堆主要是由四个Space和多个辅助数据结构组成,四个Space分别是Zygote Space、Allocation Space、Image Space和Large Object Space。Zygote Space、Allocation Space和DVM中的作用是一样的。Image Space用来存放一些预加载类,Large Object Space用来分配一些大对象(默认大小为12k)。其中Zygote Space和Image Space是进程间共享的。 

采用Mark-Sweep GC的运行时堆空间划分如下图所示。

DVM和ADT(1).png

除了这四个Space,ART的Java堆中还包括两个Mod Union Table,一个Card Table,两个Heap Bitmap,两个Object Map,以及三个Object Stack。


2、ART、DVM、JVM的GC

1、DVM的GC

格式为:D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>
引起GC原因:

    GC_CONCURRENT:当堆开始填充时,并发GC可以释放内存。
    GC_FOR_MALLOC:当堆内存已满时,app尝试分配内存而引起的GC,系统必须停止app并回收内存。
    GC_HPROF_DUMP_HEAP:当你请求创建 HPROF 文件来分析堆内存时出现的GC。
    GC_EXPLICIT:显示的GC,例如调用System.gc()(应该避免调用显示的GC,信任GC会在需要时运行)。
    GC_EXTERNAL_ALLOC:仅适用于 API 级别小于等于10 ,用于外部分配内存的GC。
其他信息:
    Amount_freed:本次GC释放内存的大小。
    Heap_stats:堆的空闲内存百分比 (已用内存)/(堆的总内存)。
    External_memory_stats:API 级别 10 及更低级别的内存分配 (已分配的内存)/(引起GC的阀值)。

    Pause time:暂停时间,更大的堆会有更长的暂停时间。并发暂停时间显示了两个暂停:一个出现在垃圾收集开始时,另一个出现在垃圾收集快要完成时。

示例:D/dalvikvm: GC_CONCURRENT freed 2012K, 63% free 3213K/9291K, external 4501K/5161K, paused 2ms+2ms

GC日志的含义为:引起GC的原因是GC_CONCURRENT;本次GC释放的内存为2012K;堆的空闲内存百分比为63%,已用内存为3213K,堆的总内存为9291K;暂停的总时长为4ms。

2、ART的GC

ART的GC日志与DVM不同,ART 不会为没有明确请求的垃圾收集打印GC日志。只有在认为GC速度慢时才会打印GC日志,更确切来说,仅在GC暂停超过5ms 或GC持续时间超过 100ms 时才会打印GC日志。如果app未处于可察觉的暂停进程状态,那么它的GC不会被认为是慢速的。ART的GC日志始终会记录显式的垃圾收集。

I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>

引起GC原因:
    Concurrent: 并发GC,不会使App的线程暂停,该GC是在后台线程运行的,并不会阻止内存分配。
    Alloc:当堆内存已满时,App尝试分配内存而引起的GC,这个GC会发生在正在分配内存的线程。
    Explicit:App显示的请求垃圾收集,例如调用System.gc()。与DVM一样,最佳做法是应该信任GC并避免显示的请求GC,显示的请求GC会阻止分配线程并不必要的浪费 CPU 周期。如果显式的请求GC导致其他线程被抢占,那么有可能会导致 jank(App同一帧画了多次)。
    NativeAlloc:Native内存分配时,比如为Bitmaps或者RenderScript分配对象, 这会导致Native内存压力,从而触发GC。 CollectorTransition:由堆转换引起的回收,这是运行时切换GC而引起的。收集器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,收集器转换仅在以下情况下出现:在内存较小的设备上,App将进程状态从可察觉的暂停状态变更为可察觉的非暂停状态(反之亦然)。
    HomogeneousSpaceCompact:齐性空间压缩是指空闲列表到压缩的空闲列表空间,通常发生在当App已经移动到可察觉的暂停进程状态。这样做的主要原因是减少了内存使用并对堆内存进行碎片整理。
    DisableMovingGc:不是真正的触发GC原因,发生并发堆压缩时,由于使用了 GetPrimitiveArrayCritical,收集会被阻塞。一般情况下,强烈建议不要使用 GetPrimitiveArrayCritical,因为它在移动收集器方面具有限制。
    HeapTrim:不是触发GC原因,但是请注意,收集会一直被阻塞,直到堆内存整理完毕。

垃圾收集器名称:
    Concurrent mark sweep (CMS):CMS收集器是一种以获取最短收集暂停时间为目标收集器,采用了标记-清除算法(Mark-Sweep)实现。 它是完整的堆垃圾收集器,能释放除了Image Space之外的所有的空间。
    Concurrent partial mark sweep:部分完整的堆垃圾收集器,能释放除了Image Space和Zygote Spaces之外的所有空间。
    Concurrent sticky mark sweep:分代收集器,它只能释放自上次GC以来分配的对象。这个垃圾收集器比一个完整的或部分完整的垃圾收集器扫描的更频繁,因为它更快并且有更短的暂停时间。

    Marksweep + semispace:非并发的GC,复制GC用于堆转换以及齐性空间压缩(堆碎片整理)

其他信息:
    Objects freed:本次GC从非Large Object Space中回收的对象的数量。
    Size_freed:本次GC从非Large Object Space中回收的字节数。
    Large objects freed: 本次GC从Large Object Space中回收的对象的数量。
    Large object size freed:本次GC从Large Object Space中回收的字节数。
    Heap stats:堆的空闲内存百分比 (已用内存)/(堆的总内存)。

    Pause times:暂停时间,暂停时间与在GC运行时修改的对象引用的数量成比例。目前,ART的CMS收集器仅有一次暂停,它出现GC的结尾附近。移动的垃圾收集器暂停时间会很长,会在大部分垃圾回收期间持续出现。

示例:I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms

GC日志的含义为:引起GC原因是Explicit ;垃圾收集器为CMS收集器;释放对象的数量为104710个,释放字节数为7MB;释放大对象的数量为21个,释放大对象字节数为416KB;堆的空闲内存百分比为33%,已用内存为25MB,堆的总内存为38MB;GC暂停时长为1.230ms,GC总时长为67.216ms。


3、Android内存泄露

    内存泄漏就是指没有用的对象到GC Roots是可达的(对象被引用),导致GC无法回收该对象。

内存泄漏的场景:

    1. 非静态内部类的静态实例
        非静态内部类会持有外部类实例的引用,如果非静态内部类的实例是静态的,就会间接的长期维持着外部类的引用,阻止被系统回收。
    2. 匿名内部类的静态实例
        和前面的非静态内部类一样,匿名内部类也会持有外部类实例的引用。
    3. Handler内存泄漏
        Handler的Message被存储在MessageQueue中,有些Message并不能马上被处理,它们在MessageQueue中存在的时间会很长,这就会导致Handler无法被回收。如果Handler 是非静态的,则Handler也会导致引用它的Activity或者Service不能被回收。
    4. 未正确使用Context
        对于不是必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们可以考虑使用Application Context来代替Activity的Context,这样可以避免Activity泄露
    5. 静态View
        使用静态View可以避免每次启动Activity都去读取并渲染View,但是静态View会持有Activity的引用,导致Activity无法被回收,解决的办法就是在onDestory方法中将静态View置为null。
    6. WebView
        不同的Android版本的WebView会有差异,加上不同厂商的定制ROM的WebView的差异,这就导致WebView存在着很大的兼容性问题。WebView都会存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。通常的解决办法就是为WebView单开一个进程,使用AIDL与应用的主进程进行通信。WebView进程可以根据业务需求,在合适的时机进行销毁。
    7. 资源对象未关闭
        资源对象比如Cursor、File等,往往都用了缓冲,不使用的时候应该关闭它们。把他们的引用置为null,而不关闭它们,往往会造成内存泄漏。因此,在资源对象不使用时,一定要确保它已经关闭,通常在finally语句中关闭,防止出现异常时,资源未被释放的问题。
    8. 集合中对象没清理
        通常把一些对象的引用加入到了集合中,当不需要该对象时,如果没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就会更加严重。
    9. Bitmap对象
        临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原始bitmap所占用的空间。 
避免静态变量持有比较大的bitmap对象或者其他大的数据对象,如果已经持有,要尽快置空该静态变量。
    10. 监听器未关闭
        很多系统服务(比如TelephonyMannager、SensorManager)需要register和unregister监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动add的Listener,要记得在合适的时候及时remove这个Listener。

    11. 其他优化
        ArrayMap 及 SparseArray 是 android 的系统 API,是专门为移动设备而定制的。 用于在一定情况下取代 HashMap 而达到节省内存的目的。 对于 key 为 int 的 HashMap 尽量使用 SparceArray 替代,大概可以省 30% 的内存,而对于其他类型,ArrayMap 对内存的节省实际并不明显,10% 左右,但是数据量在 1000 以上时,查找速度可能会变慢。
在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用 StringBuilder 来替代频繁的 “+”。
        枚举: Android 平台上枚举是比较争议的,在较早的 Android 版本,使用枚举会导致包过大,使用枚举甚至比直接使用 int 包的 size 大了 10 多倍。 在 stackoverflow 上也有很多的讨论, 大致意思是随着虚拟机的优化,目前枚举变量在 Android 平台性能问题已经不大,而目前 Android 官方建议,使用枚举变量还是需要谨慎,因为枚举变量可能比直接用 int 多使用 2 倍的内存。
        View 复用: 使用 ListView 时 getView 里尽量复用 conertView,同时因为 getView 会频繁调用,要避免频繁地生成对象。 优先考虑使用 RecyclerView 代替 ListView。
重复的布局优先使用 ,使用 减少 view 的层级,对于可以延迟初始化的页面,使用 。
        谨慎使用多进程: 现在很多 App 都不是单进程,为了保活,或者提高稳定性都会进行一些进程拆分,而实际上即使是空进程也会占用内存(1M 左右),对于使用完的进程,服务都要及时进行回收。

        系统资源: 尽量使用系统组件,图片甚至控件的 id。 例如:@android:color/xxx,@android:style/xxx


4、Android内存溢出

内存引用
    强引用:强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。 当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
    软引用:如果一个对象只具有软引用,但内存空间足够时,垃圾回收器就不会回收它;直到虚拟机报告内存不够时才会回收, 只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
    弱引用:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
    虚引用:虚引用可以理解为虚设的引用,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。 虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。 如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

1、释放强引用

一般我们在声明对象变量时,使用完后就不管了,认为垃圾回收器会帮助我们回收这些对象所指向的内存空间,实际上如果这个对象的内存空间还处在被引用状态的话,垃圾回收器是永远不会回收它的内存空间的,只有当这个内存空间不被任何对象引用的时候,垃圾回收器才会去回收。
使用完对象后,可以把对象置为空,这样我的垃圾回收器gc就会在合适的时候释放掉为该对象分配的内存空间
    Object obj = new Object();    
    obj = null; 

当然,在置为空前要确认是否不再需要使用该对象了,如果需要随时使用这个对象,则不能这么做

2、使用软引用
在jvm报告内存不足之前会清除所有的软引用,这样的话gc就可以收集到很多软引用释放出来的内存空间,从而解决内存吃紧的问题,避免内存溢出,什么时候被回收取决于gc的算法和gc运行时可用的内存大小。
用SoftReference来封装强引用的对象
    String str = "qiangyinyong";     // 强引用    

    SoftReference<String> strSoft = new SoftReference<String>(str);     // 使用软引用封装强引用

3、使用弱引用
gc收集弱引用对象的执行过程和软引用一样,只是gc不会根据内存情况来决定是否回收弱引用的对象。
    String str = "ruoyinyong";     // 强引用  
    WeakReference<String> strWeak = new WeakReference<String>(str);     // 使用弱引用封装强强引用  

如果希望能够随时取得某个对象的信息,但又不希望影响该对象的垃圾回收,则应该使用WeakReference来记住该对象,而不是使用一般的Reference。

4、图像处理
    大部分的OOM都是发生在图片加载上的,当我们加载大图时,需要特别注意避免OOM的发生。处理大图片时,不管你的手机内存有多大,如果不对图片进行处理,都有可能会发生内存溢出问题。因为Android系统会为每一个应用分配一定大小的内存,并不会把整个系统内存全部分给应用,所以不管你手机内存多大,对每个App来说,它能使用的内存都是有限的。这和PC端是有很大的不同,PC端如果内存不够了还可以请求使用虚拟内存,而Android系统可没这个机制。
1、在内存中压缩图片
装载大图片时需要对图片进行压缩,使用等比例压缩的方法直接在内存中处理图片
    Options options = new BitmapFactory.Options();    
    options.inSampleSize = 5; // 原图的五分之一,设置为2则为二分之一    
    BitmapFactory.decodeFile(myImage.getAbsolutePath(), options);  

注意:图片质量会变差,inSampleSize设置的值越大,图片质量就越差,不同的手机厂商缩放的比例可能不同。

2、使用完图片后回收图片所占内存
由于Android外层是使用java而底层使用的是C语言在里层为图片对象分配的内存空间。
所以我们的外部虽然看起来释放了,但里层却并不一定完全释放了,我们使用完图片后最好再释放掉里层的内存空间。
    if (!bitmapObject.isRecyled()) {     // Bitmap对象没有被回收    
         bitmapObject.recycle();     // 释放    
         System.gc();     // 提醒系统及时回收    

    }  

3、降低要显示的图片色彩质量
Android中Bitmap有四种图片色彩模式:
ALPHA_8:每个像素需要占用内存中的1byte
RGB_565:每个像素需要占用内存中的2byte
ARGB_4444:每个像素需要占用内存中的2byte
ARGB_8888:每个像素需要占用内存中的4byte
创建Bitmap时,默认的色彩模式是ARGB_8888的,这种色彩模式是质量最高的,当然这样的模式占用的内存也最大。而ARGB_4444每个像素只占用2byte,所以使用ARGB_4444的模式也能降低图片占用的内存大小。
    BitmapFactory.Options options = new BitmapFactory.Options();    
    options.inPreferredConfig = Bitmap.Config.ARGB_4444;    
    Bitmap btimapObject = BitmapFactory.decodeFile(myImage.getAbsolutePath(), options);  

其实大多数图片设置成ARGB_4444模式后,在显示上是看不出与ARGB_8888模式有什么差别的,只是在具有渐变色效果的图片时,可能会让渐变色呈现色彩条样的效果。这种降低色彩质量的方法对内存的降低效果不如方法1明显。

4、查询图片信息时不把图片加载到内存中
有时候取得一张图片,也许只是为了获得这个图片的一些信息,比如图片的width、height等信息,不需要显示到界面上,这个时候可以不把图片加载到内存中。
    BitmapFactory.Options options = new BitmapFactory.Options();    
    options.inJustDecodeBounds = true;     // 不把图片加载到内存中    
    Bitmap btimapObject = BitmapFactory.decodeFile(myImage.getAbsolutePath(), options); 

    inJustDecodeBounds属性,如果值为true,那么将不返回实际的Bitmap对象,也不给其分配内存空间,但允许我们查询图片宽、高、大小等基本信息。(获取原始宽高:options.outWidth,options.outHeight)


参考:

http://blog.csdn.net/itachi85/article/details/72861179
http://blog.csdn.net/itachi85/article/details/73149305
http://blog.csdn.net/itachi85/article/details/73522042
http://blog.csdn.net/itachi85/article/details/74908031
http://blog.csdn.net/itachi85/article/details/77075455
http://blog.csdn.net/freekiteyu/article/details/78060938
http://blog.csdn.net/feitian_666/article/details/51780946

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值