前言

很高兴遇见你~

内存优化一直是 Android 开发中的一个非常重要的话题,他直接影响着我们 app 的性能表现。但这个话题涉及到的内容很广且都偏向底层,让很多开发者望而却步。同时,内存优化更加偏向于“经验知识”,需要在实际项目中去应用来学习。

因而本文并不想深入到底层去讲内存优化的原理,而是着眼于宏观,聊聊 android 是如何分配和管理内存、在内存不足的时候系统会如何处理以及会对用户造成什么样的影响。

Android 应用基于 JVM 语言进行开发,虽然 google 根据移动设备特点开发了自家的虚拟机如 Dalvik、ART,但依旧是基于 JVM 模型,在堆区分配对象内存。因此 Java heap(java 堆)是android应用内存分配和回收的重点。其次,移动设备的 RAM 非常有限,如何为进程分配以及管理内存也是重中之重。

文章的主要内容是分析 Java heap、RAM 的内存管理,以及当内存不够时 android 会如何处理。

那么,我们开始吧。

Java Heap

Java Heap,也就是 JVM 中的堆区。简单回顾一下 JVM 中运行时数据区域的划分:

image.png
  • 橙色区域的方法栈以及程序计数器属于线程私有,主要存储方法中的局部数据。
  • 方法区主要存储常量以及类信息,线程共享。
  • 堆区主要负责存储创建的对象,几乎一切对象的内存都在堆区中分配,同时也是线程共享。

我们在 android 程序中使用如 Object o = new Object() 代码创建的对象都会在堆区中分配一块内存进行存储,具体如何分配由虚拟机解决而不需要我们开发者干预。当一个对象不再使用时, JVM 中具有垃圾回收机制(GC),会自动释放堆区中无用的对象,重新利用内存。当我们请求分配的内存已经超过堆区的内存大小,则会抛出 OOM 异常。

在 android 中,堆区是一个由 JVM 逻辑划分的区域,他并不是真正的物理区域。堆区并不会直接映射和他等量大小的物理内存,而是到了需要使用时,才会去建立逻辑地址和物理地址的映射:

image.png

这样可以给应用分配足够的逻辑内存大小,同时也不必在启动时一次性分配一大块的物理内存。在相同大小的内存中,可以运行更多的程序。

当堆区进程 GC 之后,释放出来多余的空闲内存,会返还给系统,减少物理内存的占用。但这个过程涉及到比较复杂的系统调用,若释放的内存较为少量,可能得不偿失,则无需返还给系统,在堆区中继续使用即可。

在 GC 过程中,如果一个对象不再使用,但是其所占用的内存无法被释放,导致资源浪费,这种现象称为内存泄漏。内存泄露会导致堆区中的对象越来越多,内存的压力越来越大,甚至出现 OOM 错误。因此,内存泄露是我们必须要尽量避免的现象。

进程内存分配

堆区的内存分配,属于进程内的内存分配,由进程自己管理。下面讲一下一个应用,系统是如何为其分配内存的。

系统的运行内存,即为我们常说的 RAM ,每个应用必须装入内存中才可以被执行:

image.png
  • 我们安装的应用进程都位于硬盘中

  • 当一个应用被执行时,需要装入到 RAM 中才能被执行(zRAM 是为了压缩数据节省空间而设计,后续会讲到)

  • CPU 与 RAM 交互,读取指令、数据、写入数据等

    RAM 的大小为设备的硬件内存大小,是非常宝贵的资源。现代手机常见的运存是6G、8G或者12G,一些专为游戏研发的手机甚至有18G,但同时价格也会跟上去。所以,RAM的资源是非常有限且珍贵的,需要我们好好去利用他。

Android 采用分页存储的方式把一个进程存储到 RAM 中。分页存储,简单来说就是把内存分割成很多个小块,每个应用占用不同的小块,这些小块也可以称为页:

前面讲到,进程的堆区并不是一次性分配,当需要分配内存时,系统会为其分配空闲的页;当这些页被回收,那么有可能被返还到系统中。

这里的页、块概念涉及到操作系统的分页存储,这里并不打算展开详细讲解,有兴趣的读者可以自行了解:分页存储-维基百科。本文中的“页”与“块”可以不严谨地理解为同个概念,为了帮助理解这里不进行详细地区分。

分配给进程的页可以分为两种类型:干净页、脏页:

  • 干净页:进程从硬盘中读取数据或申请内存之后未进行修改。这种类型的页面在内存不足的时候可以被回收,因为页中存储的数据可通过其他的途径复原。
  • 脏页:进程对页中的数据进行了修改或数组存储。这类页面不能被直接回收,否则会造成数据丢失,必须先进行数据存储。

zRAM,是作为 RAM 中的一个分区,他可以把一些类型的页压缩之后存储在zRAM中,当需要使用的时候再从zRAM中调出。通过压缩来节省应用的空间占用,同时不需要与硬盘进行调度,提高了速度。

这里需要理解的一个点是:内存中的操作速度要远远比硬盘操作快。即使与zRAM的调入和调出需要压缩和解压,其速度也是比与硬盘交互快得多。

但是在实际中,RAM 并不会存在大量的严谨意义上的未利用页。每个进程退出之后并不会马上从内存中被换出,而是会缓存在 RAM 中,当内存不足时才会去释放他。这样的好处是,下一次打开最近使用的 app 的时候,可以更加快速地启动。

内存不足管理

前面我们一直强调,移动设备的内存容量是非常有限的,需要我们非常谨慎地去使用它。幸运的是,JVM 和 android 系统早就帮我们想到了这一点。

面对不同的内存压力,android 会有不同的应对策略。从低到高依次是 GC、内核交换守护进程释放内存、低内存终止守护进程杀死进程释放内存;他们的代价也是逐步上升。下面我们依个来介绍一下。

GC

GC 属于 JVM 内部的内存管理机制,这种内存的释放范围是在进程之内。

内核交换守护进程

低内存终止守护进程

为什么要内存优化

最后

全文到此,原创不易,觉得有帮助可以点赞收藏评论转发。
有任何想法欢迎评论区交流指正。
如需转载请评论区或私信告知。

另外欢迎光临笔者的个人博客:传送门