前言
众所周知,内存优化可以说是性能优化中最重要的优化点之一,可以说,如果你没有掌握系统的内存优化方案,就不能说你对Android的性能优化有过多的研究与探索;本篇将带领大家一起来系统地学习Android中的内存优化
可能有不少读者都知道,在内存管理上,JVM拥有垃圾内存回收的机制,自身会在虚拟机层面自动分配和释放内存,因此不需要像使用C/C++一样在代码中分配和释放某一块内存
Android系统的内存管理类似于JVM,通过new关键字来为对象分配内存,内存的释放由GC来回收; 并且Android系统在内存管理上有一个 Generational Heap Memory模型,当内存达到某一个阈值时,系统会根据不同的规则自动释放可以释放的内存。即便有了内存管理机制,但是,如果不合理地使用内存,也会造成一系列的性能问题,比如 内存泄漏、内存抖动、短时间内分配大量的内存对象 等等
什么是内存?
首先看下这里的内存到底指的是什么?可以看下面这张图:
● 手机中主要的存储部分分两块RAM和ROM,RAM存储程序的运行时数据,设备关机就会清空,我们也称之为内存;ROM也就是磁盘,存放一些永久的数据
● 上图我们看到这个RAM中还有一个zRAM分区,这个zRAM分区会在内存不足时发挥作用
● 到这里简单介绍了手机的内存是指什么,当我们不断打开APP时,手机的内存会被占的越来越多
低内存终止守护进程
很多时候,kswapd 不能为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory()通知应用内存不足,应该减少其分配量。如果这还不够,内核会开始终止进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作
LMK 使用一个名为 oom_adj_score 的“内存不足”分值来确定正在运行的进程的优先级,以此决定要终止的进程。最高得分的进程最先被终止。后台应用最先被终止,系统进程最后被终止
各种类别的说明
● 后台应用:之前运行过且当前不处于活动状态的应用。LMK 将首先从具有最高 oom_adj_score 的应用开始终止后台应用
● 上一个应用:最近用过的后台应用。上一个应用比后台应用具有更高的优先级(得分更低),因为相比某个后台应用,用户更有可能切换到上一个应用
● 主屏幕应用:这是启动器应用。终止该应用会使壁纸消失
● 服务:服务由应用启动,可能包括同步或上传到云端
● 可觉察的应用:用户可通过某种方式察觉到的非前台应用,例如运行一个显示小界面的搜索进程或听音乐
● 前台应用:当前正在使用的应用。终止前台应用看起来就像是应用崩溃了,可能会向用户提示设备出了问题
● 持久性(服务):这些是设备的核心服务,例如电话和 WLAN
● 系统:系统进程。这些进程被终止后,手机可能看起来即将重新启动
● 原生:系统使用的极低级别的进程(例如,kswapd)
● 设备制造商可以更改 LMK 的行为
Android中的进程
Native进程
● 用C/C++编写的,不包含dalvik实例的进程,/system/bin/目录下面的程序文件运行后都是以native进程形式存在的。/system/bin/surfaceflinger、/system/bin/rild、procrank等就是native进程。
● 内存函数包括 MALLOC/FREE,NEW/DELETE等;动态内存需要人工管理
Dalvik进程
● Android中运行于dalvik虚拟机之上的进程,进程中存在一个虚拟机实例 dalvik虚拟机的宿主进程由fork()系统调用创建
● 一般应用开发者管辖之内的内存空间
● 每一个java进程都是存在于一个native进程中
进程的地址空间
在32位操作系统中,进程的地址空间为0到4GB,程序布局如下示意图:
这里主要说明一下Stack和Heap: Stack空间(进栈和出栈)由操作系统控制,其中主要存储函数地址、函数参数、局部变量等等
所以Stack空间不需要很大,一般为几MB大小。 Heap空间由程序控制,程序员可以使用malloc、new、free、delete等函数调用来操作这片地址空间;Heap为程序完成各种复杂任务提供内存空间,所以空间比较大,一般为几百MB到几GB;正是因为Heap空间由程序员管理,所以容易出现使用不当导致严重问题
进程内存空间和RAM之间的关系
● 虚拟内存(Virtual Memory):利用磁盘空间虚拟出的一块逻辑内存。(地址空间)
● 物理内存(Physical Memory):真实物理内存所能表达的地址空间范围(物理空间)
● 一个页被多个虚拟地址引用,并且是不可修改部分, Sharaed_Clean
● 一个页被多个虚拟地址引用,并且是被修改过的部分,Sharaed_Dirty
● 一个页只被当前进程的虚拟地址引用,并且是不可修改部分,Private_Clean
● 一个页只被当前进程的虚拟地址引用,并且是被修改过的部分,Private_Dirty
● VSS: Virtual Set Size = Size
● PSS: Proportional Set Size = (Shared_Clean + Shared_Dirty) / Num_of_shared_process + Private_Dirty +Private_Clean
● RSS: Resident Set Size = Shared_Clean+Shared_Dirty+Private_Clean+Private_Dirty
● USS: Unique Set Size = Private_Clean + Private_Dirty
一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS
● RAM作为进程运行不可或缺的资源,对Android系统性能和稳定性有着决定性影响,RAM的一部分被操作系统留作他用,比如显存等等,当然这个程序员无法干预,我们也不必过多地关注它。进程空间中的heap空间是我们需要重点关注的
● heap空间完全由程序员控制,我们使用的malloc、C++ new和java new所申请的空间都是heap空间, C/C++申请的内存空间在native heap中,而java申请的内存空间则在dalvik heap中
Android中关于内存优化的问题主要包括三个方面:
● Memory Leaks 内存泄漏
● OutOfMemory 内存溢出
● Memory Churn 内存抖动
Memory Leaks 内存泄漏
内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
● 内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测;因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃
● 随着计算机应用需求的日益增加,应用程序的设计与开发也相应的日趋复杂,开发人员在程序实现的过程中处理的变量也大量增加,如何有效进行内存分配和释放,防止内存泄漏的问题变得越来越突出
● 例如服务器应用软件,需要长时间的运行,不断的处理由客户端发来的请求,如果没有有效的内存管理,每处理一次请求信息就有一定的内存泄漏;这样不仅影响到服务器的性能,还可能造成整个系统的崩溃;因此,内存管理成为软件设计开发人员在设计中考虑的主要方面
out of memory 内存溢出
out of memory英文意思是电脑内存不足,我们都清楚,电脑程序的运行不仅仅对电脑CPU进行消耗,同时对内存也会进行占用,当占用到一定存度就会出现内存不足的情况,这时电脑系统就会出现out of memory错误提示,那么那些情况会出现out of memory情况呢
● 运行的程序相对占用内存较多,出现这种情况大多是一些特别大型的程序,例如3DsMax,Maya
● 电脑打开的程序过多,这样因程序过多点用的内存资源过多也会出现out of memory问题
● 电脑病毒感染,如果电脑中毒了,这时大量的内存被病毒点用掉了,这时同样会出现out of memory提示信息
● 电脑设置不正确或电脑程序运行配置不正确
Memory Churn 内存抖动
内存抖动是因为大量的对象被创建又在短时间内马上被释放
● 瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC
● 即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC;这个操作有可能会影响到帧率,并使得用户感知到性能问题
Android内存管理原理
系统级内存管理
Android系统内核是基于Linux,所以说Android的内存管理其实也是Linux的升级版而已
● Linux在进程停止后就结束该进程,而Android把这些停止的进程都保留在内存中,直到系统需要更多内存时才选择性的释放一些,保留在内存中的进程默认(不包含后台service与Thread等单独UI线程的进程)不会影响整体系统的性能(速度与电量等)当再次启动这些保留在内存的进程时可以明显提高启动速度,不需要再去加载
● 再直白点就是说Android系统级内存管理机制其实类似于Java的垃圾回收机制,这下明白了吧;在Android系统中框架会定义如下几类进程、在系统内存达到规定的不同level阈值时触发清空不同level的进程类型
可以看见,所谓的我们的Service在后台跑着跑着挂了,或者盒子上有些大型游戏启动起来就挂(之前我在上家公司做盒子时遇见过),有一个直接的原因就是这个阈值定义的太大,导致系统一直认为已经达到阈值,所以进行优先清除了符合类型的进程
所以说,该阈值的设定是有一些讲究的,额,扯多了,我们主要是针对应用层内存分析的,系统级内存回收了解这些就基本够解释我们应用在设备上的一些表现特征了
应用级内存管理
● 在说应用级别内存管理原理时大家先想一个问题,假设有一个内存为1G的Android设备,上面运行了一个非常非常吃内存的应用,如果没有任何机制的情况下是不是用着用着整个设备会因为我们这个应用把1G内存吃光然后整个系统运行瘫痪呢?
● 其实Google的工程师才不会这么傻的把系统设计这么差劲;为了使系统不存在我们上面假想情况且能安全快速的运行,Android的框架使得每个应用程序都运行在单独的进程中(这些应用进程都是由Zygote进程孵化出来的
● 每个应用进程都对应自己唯一的虚拟机实例);如果应用在运行时再存在上面假想的情况,那么瘫痪的只会是自己的进程,不会直接影响系统运行及其他进程运行
● 既然每个Android应用程序都执行在自己的虚拟机中,那了解Java的一定明白,每个虚拟机必定会有堆内存阈值限制(值得一提的是这个阈值一般都由厂商依据硬件配置及设备特性自己设定,没有统一标准,可以为64M,也可以为128M等;它的配置是在Android的属性系统的/system/build.prop中配置dalvik.vm.heapsize=128m即可
● 若存在dalvik.vm.heapstartsize则表示初始申请大小),也即一个应用进程同时存在的对象必须小于阈值规定的内存大小才可以正常运行
● 接着我们运行的App在自己的虚拟机中内存管理基本就是遵循Java的内存管理机制了,系统在特定的情况下主动进行垃圾回收;但是要注意的一点就是在Android系统中执行垃圾回收(GC)操作时所有线程(包含UI线程)都必须暂停,等垃圾回收操作完成之后其他线程才能继续运行。这些GC垃圾回收一般都会有明显的log打印出回收类型
常见的如下:
● GC_MALLOC——内存分配失败时触发
● GC_CONCURRENT——当分配的对象大小超过一个限定值(不同系统)时触发
● GC_EXPLICIT——对垃圾收集的显式调用(System.gc())
● GC_EXTERNAL_ALLOC——外部内存分配失败时触发
通过上面这几点的分析可以发现,应用的内存管理其实就是一个萝卜一个坑,坑都一般大,你在开发应用时要保证的是内存使用同一时刻不能超过坑的大小,否则就装不下了
总结
综上所述,对应用进行内存优化已然成为当下开发工程师应该具备的基本技能之一,也是对开发工程师是否有能力维护高质量应用程序的重要考核之一;另外,内存优化是一个非常具有挑战性的工作,想要进行完美的内存优化绝非一日之功,需要考验开发者长期研究的耐心和深厚的技术功底
当然如果在我们开发中只是一味的追求各种极致的优化也是不对的,因为优化本来就是存在风险的,甚至有些过度的优化会直接导致项目的臃肿;所以不要因为极致的性能优化而破坏掉了你项目的合理架构
总之一句话,优化适可而止,请酌情优化
尾述
技术是无止境的,你需要对自己提交的每一行代码、使用的每一个工具负责,不断挖掘其底层原理,才能使自己的技术升华到更高的层面
Android 架构师之路还很漫长,与君共勉
PS:有问题欢迎指正,可以在评论区留下你的建议和感受; 欢迎大家点赞评论,觉得内容可以的话,可以转发分享一下
网友评论