最近一直在做内存和 ANR 相关的优化,接下来我将会花几篇文章梳理一下内存相关的优化,以及我是如何将 OOM 崩溃率下降 90%。 今天这篇文章主要介绍内存相关的知识点,以及那些因素会导致 OOM 崩溃和相对应的解决方案,所以通过这篇文章你将学习到以下内容:
不知道小伙伴们有没有经历过,相同的优化方案,A 应用上线之后,崩溃率下降很多,但是 B 应用上线只有一点点收益,每个优化方案,在不同的 App 上所得到的优化效果未必一样,因为每个 App 在不同的国家和地区面对的用户群体不一样,因此机型也都不一样,所以我们需要了解内存相关的知识点,结合线上和线下数据,对自己的 App 进行归因,对症下药,才能取得较大的收益。 内存是极其稀缺的资源,不合理的使用会导致可用内存越来越少,可能会引发卡顿、ANR、OOM 崩溃、Native 崩溃等等,严重影响用户的体验。所以当我们在做性能优化的时候,内存优化是非常重要的环节。 初期在做内存优化的时候,在我们的脑海里都会有一个潜意识「内存占用越少越好」,在某些情况下是不对的。例如在高端机上我们可以多分配点内存,可以提升用户的体验,但是在低端机上内存本身就很小,所以我们应尽量减少内存的分配。例如针对损耗性能的动画、特效等等,在低端机上是不是可以关掉,或者关掉硬件加速、采用其他的方案代替,这样不仅可以减少崩溃,还可以减少卡顿,提高用户体验。 因为 Java 有自动回收机制,所以在开发过程中,很少有人会去关心内存问题,在脑海中都会有一个潜意识 GC 会自动回收,所以用完不会主动释放掉无用资源例如 Bitmap、动画、播放器等等,等待 GC 来回收,在实际项目中,依赖 GC 是不可靠的。首先 GC 自动回收机制具有不确定性,GC 也分为了不同的类型,如果发生 Full GC 时,会触发 stop the work 事件,会使 App 变得更加严重。 另外 GC 的回收机制根据可达性分析算法判断一个对象是否可以被回收,如果存在内存泄露,GC 是不会回收这些资源的,逐渐累积,当达到堆的内存上限时,发生 OOM 崩溃了,所以你要保证自己不要写出内存泄露的代码,以及团队其他人不要写出内存泄露的代码,然而实际情况这是不可能的,所以依靠 GC 自动回收机制这种想法是不可靠的。虽然 Java 有内存回收机制,但是我们应该在脑海中保留内存管理的意识,所以当申请完内存,退出或者不在使用时,及时释放掉内存。真正做到 用时分配,及时释放。 可用内存越来越少时,严重时会导致 OOM 崩溃,做过 OOM 优化的朋友应该会发现,线上捕获的大部分 OOM 崩溃堆栈,都是压死骆驼的最后一根稻草,并不是问题的根本所在,所以我们需要对 OOM 崩溃进行归因,找到占用内存的大头。降低整机已使用的内存,从而降低 OOM 崩溃,因此我大概分为了以下几个方面。
其中 FD 和线程崩溃占比很低,因此这不是我们前期优化的重点。这篇文章我们重点介绍 虚拟内存和物理内存,下篇文章将会介绍堆内存, 堆内存是程序在运行过程中为对象分配内存的区域,它也属于虚拟内存的范围。
介绍虚拟内存之前,我们需要先介绍物理内存,物理内存就是实实在在的内存(即内存条),如果应用直接对物理内存操作,会存在很多问题:
为了解决上面的问题,我们需要为每个应用分配 “中间内存” 最终会映射到物理内存上,这就是接下来要说的虚拟内存。
操作系统会为每个应用分配一个独立的虚拟内存,实现应用间的内存隔离,避免了应用 A 修改应用 B 的内存数据的问题,虚拟内存最终会映射到物理内存上,当应用申请内存时,得到的是虚拟内存,只有真正执行写操作时,才会分配到物理内存,好处是应用可以使用连续的地址空间来访问不连续的物理内存。
每个应用程序可使用的虚拟内存大小受 CPU 位宽及内核的限制。我们常说的 16 位 cpu,32 位 cpu,64 位 CPU,指的都是 CPU 的位宽,表示的是一次能够处理的数据宽度,即 CPU 能处理的 2 进制位数,即分别是 16bit,32bit 和 64bit。而目前市面上常用的是 32 位和 64 的设备。
32 位设备可以使用的虚拟内存大小 3GB
32 位 CPU 架构的设备可使用的地址空间大小为 2^32=4GB, 虚拟内存空间分为 内核空间 和 用户空间,系统提供了三种虚拟地址空间分配的参数,代表用户空间可访问的虚拟地址空间大小。
64 位应用可以使用的虚拟内存大小 512GB
64 位 CPU 架构的设备虽然拥有 64 位的地址空间,但是不是全部都可以使用的,为了后期的扩展,只能使用部分地址。 Android 默认的虚拟地址的长度配置为 CONFIG_ARM64_VA_BITS=39,即 Android 的 64 位应用可使用的地址空间大小为 2^39=512GB。 当 32 位应用在 64 位的设备上运行时,可使用 4GB 虚拟地址空间,而 64 位应用可使用 512GB 的空间。因此在 64 位机器上不存在虚拟空间不足的问题。因此在 2019 年的时候 Google Play 要求除了提供 32 位的版本之外,还需要提供 64 位的版本。 在我们的 OOM 崩溃设备中,32 位的设备占比 50%+ 以上,虚拟内存不足主要发生在 32 位的设备上。
在 32 位的设备上,受地址空间最大内存 4 GB 限制,内核空间占用 1G,剩下的 3G 是用户空间,我们可以通过解析 /process/pid/smaps 文件,查看当前虚拟内存分配情况。
系统资源预分配,包含了 Zygote 进程初始化时,需要加载 Framework 层的代码和资源。供 Fork 出来的子进程可以直接使用。 Framework 资源包含:Framework 层 Java 代码、so、art 虚拟机、各种静态资源字体、文件等等 系统预分配区域中其中 [anon:libwebview reservation] 区域占用 130MB 内存 App 自身资源,包括 App 中的代码、资源、 App 直接或者间接开启线程消耗的栈空间、 App 申请的内存、内存文件映射等内容。
Java 堆用于分配 Java / Kotlin 创建的对象。由 GC 管理和回收,GC 回收时将 From Space 里的对象复制到 To Space,这两片区域分别为 dalvik-main space 和 dalvik-main space 1, 这两片区域的大小和我当前测试机 Java 堆大小一样,都是 512 MB,如下图所示
根据 Android 源码中的解释,Java 堆的大小应该是根据 RAM Size 来设置的,这是一个经验值,厂商是可以更改的,如果手机 Root 之后,自己也可以改,无论 RAM 多大,到目前为止 Java 堆的上限默认都是 512MB, Google 源码的设置如下如下图所示。
经过分析内核、系统资源、以及各 APP 的资源占用,最后留给我们使用的内存并不是很多,所以我们要合理使用系统资源,真正做到 “用时分配,及时释放”。
目前业界也有很多黑科技来释放因系统占用的虚拟内存不足的问题,大概有以下几个方面的优化。
Android 11 之前使用的垃圾回收器是 jemalloc,Android 11 之后默认使用的垃圾回收器是 scudo。
下图是 App 在 Android 7.0 上启动完成之后所占用的虚拟内存 (Vss),不同系统、不同的 App 虚拟内存的分布都不一样,,我们可以通过解析 /process/pid/smaps 文件,查看自己的 App 虚拟内存分配情况。
正如上图所示,主要分为三个部分:
针对上面的问题,我们在项目中通过以下手段进行优化,重点优化 dalvik 占用的内存,因篇幅问题,将会在后面的文章中,做详细的分析:
Java 堆上还有很多可用的内存,为什么还会出现 OOM
很多小伙伴们都问过我这么一个问题,大概归因了一下,主要有以下几个原因:
文章的最后想提一点,我们在做性能优化的时候,不仅要关心性能指标数据,还需要关心对业务指标数据的影响,比如对使用时长、留存等等能提升多少。
为什么需要关心业务指标数据?
性能指标数据,比如 OOM 崩溃率、Native 崩溃率、ANR 等等、可能只有客户端的小伙伴才知道 OOM、Native、ANR 是什么意思,但是其他人(产品经理、老板等等)他们是不知道的,也不会去关心这些,但是他们对使用时长、留存等业务指标数据更加的敏感,更能够体现做这件事的价值,这只是阐述了我自己的观点,每个人站的角度不一样,观点也不一样。 全文到这里就结束了,这篇文章只是梳理一下内存相关的知识点,以及有那些因素会导致 OOM 崩溃和相对应的解决方案。
源码附件已经打包好上传到百度云了,大家自行下载即可~
链接: https://pan.baidu.com/s/14G-bpVthImHD4eosZUNSFA?pwd=yu27 提取码: yu27 百度云链接不稳定,随时可能会失效,大家抓紧保存哈。
如果百度云链接失效了的话,请留言告诉我,我看到后会及时更新~
码云地址: http://github.crmeb.net/u/defu
Github 地址: http://github.crmeb.net/u/defu
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。