性能优化实践(四)-内存优化思考

一、应用层

对应用层来说,最主要的内存问题还是内存泄漏问题。

Java中的内存分配

静态储存区:编译时就分配好,在程序整个运行期间都存在。它主要存放静态数据和常量;
栈区:当方法执行时,会在栈区内存中创建方法体内部的局部变量,方法结束后自动释放内存;
堆区:通常存放 new 出来的对象。由 Java 垃圾回收器回收。

Java中的内存泄漏

root可达,但对象本身已无用。造成问题的原因是:生命周期长的对象持有了生命周期短对象的引用,即使生命周期短的对象完成了任务,但是由于还继续被持有引用,造成无法被回收。

四种引用类型的介绍

强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;
软引用(SoftReference):只有在内存空间不足时,才会被回的对象;
弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

常见的内存泄漏的场景包含如下几类:

1 静态造成的泄漏

静态对象的生命周期与应用程序一样长,持有对象的引用基本都是泄漏,因为一般对象的生命周期都不会有它长。

举例:

  • 单例造成的内存泄露;
  • 静态集合保存对象;
  • 全局静态大对象。

解决方案:

  • 如果需要持有对象引用,将该对象的引用方式改为弱引用;
  • 如果需要持有Context,使用ApplicationContext;
  • 大对象本身尽量别做成静态的

2 非静态内部类和匿名类造成的泄漏

在Java中,非静态内部类 和 匿名类 都会潜在的引用它们所属的外部类,但是,静态内部类却不会。如果这个非静态内部类实例做了一些耗时的操作,就会造成外围对象不会被回收,从而导致内存泄漏。

举例:

这部分常见的有Thread、Handler、AsyncTask作为Activity内部类使用时,当然也包括自定义的内部类。

解决方案:

  • 将内部类变成静态内部类;
  • 如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用
  • 在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务;

3 Activity Context 的不正确使用

在Android应用程序中通常可以使用两种Context对象:Activity和Application。当类或方法需要Context对象的时候常见的做法是使用第一个作为Context参数。这样就意味着View对象对整个Activity保持引用,因此也就保持对Activty的所有的引用。

假设一个场景,当应用程序有个比较大的Bitmap类型的图片,每次旋转是都重新加载图片所用的时间较多。为了提高屏幕旋转是Activity的创建速度,最简单的方法时将这个Bitmap对象使用Static修饰。 当一个Drawable绑定在View上,实际上这个View对象就会成为这份Drawable的一个Callback成员变量。而静态变量的生命周期要长于Activity。导致了当旋转屏幕时,Activity无法被回收,而造成内存泄露。

解决方案:

  • 使用ApplicationContext代替ActivityContext,因为ApplicationContext会随着应用程序的存在而存在,而不依赖于activity的生命周期;
  • 对Context的引用不要超过它本身的生命周期,慎重的对Context使用“static”关键字。Context里如果有线程,一定要在onDestroy()里及时停掉。

下图可以参考下什么场景下选择ApplicationContext,建议是能用ApplicationContext的就不用ActivityContext:

4 代码习惯

Cursor,Stream没有close,Bitmap没有recyle,注册监听没有取消注册,集合对象不使用之后,内部对象没有及时清理掉, Adapter没有使用convertView等等。

解决方案:按规范来。

5 大View造成的泄漏

这里所谓的大View比如:WebView,当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。

解决方案:

  • 为webView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

VideoView也同样如此,业务条件允许,尽量全局使用一个VideoView。

如果这些你都熟练于心的话,那在日常开发中就会避免掉不少主流的泄漏问题。未雨绸缪,防范于未然是非常重要的,毕竟内存泄漏很大程度上就是一个量变引起质变的问题。

6 谨慎使用三方库

当然,避免归避免,内存泄漏还是有可能会发生的。发生之后如何分析?

常用的内存泄漏排查流程:

1)adb shell dumpsys meminfo packageName

先通过dumpsys ,宏观了解下当前进程的内存具体分配情况,看看dalvik heap、native heap等等内存占用情况,大致了解下泄漏发送在java层还是native层,或者graphic跟视图、SurfaceView、图片相关的内存占用情况。

2)LeakCanary

用LeakCanary扫一遍,可以非常高效地先检查出Activity/Fragment的内存泄漏。触发的点是:Activity/Fragment执行onDestroy回调,它的原理大概是:

当然,它无法覆盖所有问题,最底层的activity因为很难调用onDestroy,所以检测不到,这里倒是可以配合dumpsys meminfo的Objects部分通过back退出app,activitys 是否为0来帮助看最底层的activity是否有泄漏。另外,Service的内存泄漏也无法检测。

3)AS memory monitor / Heap Viewer

经过LeakCanary的大扫除,大部分问题已经被解决了,那么还剩下一部分问题如何处理呢?
如果你更喜欢视图:AS memory monitor ,尽量使用AS 3.0之后的profiler。
如果你更喜欢数据:Heap Viewer。
操作app,AS memory monitor 中视图曲线是否存在不断上升的趋势且不会在程序返回时明显回落。或者Heap Viewer中free值是不是逐渐减少,两种方式都提供手动GC按钮,GC之后看内存回调数值是不是期望的。对可疑点可以进行反复多次的操作,缩小怀疑范围。

4)MAT

dump hprof文件,一般我喜欢再转一下格式,用MAT打开分析,不转的话只能用AS分析,但是MAT功能更全面。一般会生成两份hprof,一份是正常情况的,一份是怀疑存在泄漏的,针对怀疑的点进行对比,是否有泄漏。

主要看:
with outgoing references: 查看它所引用的对象
with incoming references: 查看它被哪些对象引用

然后排除掉软弱虚引用,剩下的再去分析是否存在泄漏,如果没有泄漏,那再重复怀疑重复分析,具体分析MAT工具篇有详细说明。

泄漏的问题基本上就到这了,下面我再分析下应用程序其他的内存问题:

内存抖动:短时间内大量创建和回收对象,造成频繁GC,Art虚拟机在Dalvik的基础上针对GC做了优化,GC效率提高了2-3倍,且缩短了线程暂停时间,偶尔进行的GC暂停所有工作线程的时间几乎无感觉,但是如果频繁GC的话,那是有可能影响到用户体验的。

优化策略:

  • 避免不必的对象开销。
  • 对于就是频繁需要创建和使用对象的场景下,引入池的概念,或则设计模式中的享元模式都是可以考虑使用的。

内存瘦身
说白了就是减少不必要的内存开销,首当其冲的就是资源瘦身,也变相的是APK瘦身,能用代码实现的视图就尽量用代码,不得不用图片的,在保证一定清晰度要求情况下尽量减少图片大小,Linit检测去掉不必要的代码,三方库,用不到的代码也能去掉的尽量去掉(同时还减少了方法数)等等吧。其次缓存优化,很多时候为了提升效率,会把部分数据缓存在内存中,包括图片,不是调用很频繁的完全可以以文件的形式保存到硬盘上。另外能用生命周期短的对象就不用长的,这样能更快被回收。

二、Java Framework

这一层主要是从进程调度的角度来优化内存,详细内存之前《Android进程管理篇(三)-AMS进程调度》做过详细分析。

作为系统层,并不是内存越低越好,内存低往往体验也会差。大部分时候都是在用户体验和剩余内存上做博弈。这里我主要总结下低内存手机的内存优化思路:

1 lmk:调整合适的lmk水线,及时在内存低的时候杀掉部分进程腾出内存。减少cache进程缓存数,只要活着就会占内存,牺牲启动速度来换取内存。b-service级别进程在内存偏低时,降级为更低的cache级别,让lmk能杀到更多进程。为某些占用内存大的高日活应用增加白名单,启动时,默认清掉所有cache进程等等。

2 调整虚拟机能分配给进程的内存大小 dalvik.vm.heapgrowthlimit 和 dalvik.vm.heapsize

等等

三、Kernel层

这层主要做的优化大部分是提高系统剩余内存,也总结几个优化点吧:

1 可以针对文件页和匿名页做回收,在lru链表中,把不活跃链队尾的页尽量回收掉。提高swapiness值来倾向于回收更多匿名页,这样能增大Free RAM。

2 针对低内存手机,都会打开zram。Android 4.4 推出ZRAM内存压缩技术. zram是内核的一个模块,是内存管理的一种机制。可以将交换出去的页按照1:4的比例进行压缩存储,减小内存占用。压缩的区域依然是内存,并不是硬盘。利用这一特性,我们可以将不经常使用的进程占用内存尽可能的放在ZRAM,实现占用尽量少的内存,占用尽量多的缓存,节省内存;保证前台的速度,前台的应用或者关键应用不放在ZRAM中。

3 cat proc/meminfo 时候发现Mlocked值比较高,固化了部分文件页,目的是对于频繁使用的文件页,系统会把他们锁定到对应的page页中,减少io,方便使用,典型的空间换时间,比如google 的pinnerService就是这样,建议关闭。

4 配置MALLOC_SVELTE 来disable tcache,即jemalloc不采用tcache方法分配内存,这样的好处是占用内存少,但是分配内存速度会变慢。

另外,cat proc/meminfo

Kernel内存可使用内存 ≈ Slab + KernelStack + PageTables。

Slab: 236424 kB
SReclaimable: 66836 kB
SUnreclaim: 169588 kB

如果Slab 与 SUnreclaim 都非常高,且一段时间后也没有明显回落的话,可能存在内存泄漏。

可以通过page_owner工具抓出kernel的调用栈,用脚本捋出top问题,分析其调用栈来排查。

好了,先写这么多。一些不成熟的小建议,欢迎补充。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容