iOS - 理解内存

2,512 阅读9分钟

简介

存储器层次结构

操作系统内存对应物理存储结构中为L4主存,由于操作系统中指令集和虚拟内存2大抽象,对于操作系统而言虚拟内存是对应的操作管理的空间,虚拟内存通过MMU与物理内存作映射。

虚拟内存

通过MMU实现的虚拟寻址,提供虚拟的地址空间。对于每个进程来说,操作系统提供了一个独立私有且连续的地址空间

实现

虚拟寻址

  • 物理寻址

计算机系统的主存被组织成一个由M个连续的字节大小和单元组成的数组。每字节都有一个唯一的物理地址。第一个字节地址是0,第二个地址是1,再往下是2,依此类推

  • 虚拟寻址

CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到内存之前被转换成适当的物理地址。CPU芯片上有叫做内存管理单元(Memory Management Unit, MMU)的专用硬件,利用存放在主存中的查询表来动态翻译虚拟地址

  • 地址空间

地址空间是一个非负整数地址的有序集合

分页

和存储器层次结构中其他缓存一样,磁盘(较低层)上的数据被分割成块,这些块作为磁盘和主存(较高层)之间的传输单元。VM系统通过将虚拟内存分割成称为虚拟页(Virtual Page,VP)的大小固定的块来处理这个问题。类似地,物理内存被分割为物理页(Physical Page,PP),大小也为P字节(物理页也被称为页帧(Page Frame))

任意时刻,虚拟页面的集合都分为

  • 未分配的

VM系统还未分配或创建的页

  • 缓存的

当前已缓存在物理内存中的已分配的页

  • 未缓存的

未缓存在物理内存中的已分配的页面

作用

  • 作为缓存的工具
  • 作为内存管理的工具
  • 作为内存保护的工具

缓存

从存储器层次结构来看,DRAM比SRAM要慢50倍,SSD固态硬盘比DRAM要慢1000倍

页表

跟任何缓存一样,虚拟内存系统必须有某种方法判定一个虚拟页是否在内存中。如果在内存中,是在哪个物理页。如果不在内存中,是在磁盘哪个位置,并在物理内存中选择一个牺牲页,并将虚拟页从磁盘复制到内存中。

这些功能由操作系统,MMU中的地址翻译硬件和一个存放在物理内存中的数据结构,即页表(page table),页表将虚拟页映射到物理页。每次地址翻译硬件将一个虚拟地址转换为物理地址时,都会读取页表。

页命中

页命中的状态下,地址翻译硬件将虚拟地址作为一个索引来定位物理地址,直接从物理地址读取。

缺页

内存缓存不命中称为缺页。地址翻译硬件从内存中读取,从有效位判断未缓存,触发缺页异常。缺页异常调用内核中的缺页异常处理程序,选择牺牲页,复制虚拟页从磁盘到牺牲页。

当异常处理程序返回时,会重新启动导致缺页的指令,该指令会把导致缺页的虚拟地址重新发送到地址翻译硬件,走页命中一样的逻辑。

这边的缺页导致了需要选择内存牺牲页和从磁盘复制,导致了一次多余的IO,由于磁盘的读取速度比DRAM低得多,所以缺页会影响内存读取性能。

抖动

由于局部性原理,虽然缺页的成本比较高,但是缺页的触发通常不会频繁。虽然整个运行过程中程序引用的不同页面总数可能超过物理内存总的大小,但是局部性原理保证了在任意时刻,程序将趋向于在一个较小的活动页面(active page)集合上工作,这个集合叫做工作集(working set)或者常驻集合(resident set)。

但是如果工作集的大小超出了物理内存的大小,那么程序会产生一个不良的状态,抖动(thrashing),页面将不断换进换出,导致性能低下。

内存管理

操作系统为每个进程提供了一个独立的页表,一个独立的虚拟地址空间。按需页面调度和独立的虚拟地址空间的结合,对系统中内存的使用和管理造成了深远影响。尤其是VM简化了链接和加载,代码和数据共享,以及应用程序的内存分配。

  • 简化链接

独立的地址空间允许每个进程的内存映像使用相同的基本格式,而不管代码和数据实际存放在物理内存的位置

  • 简化加载

虚拟内存还使得容易向内存中加载可执行文件和共享对象文件

  • 简化共享

独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制

  • 简化内存分配

虚拟内存为向用户进程提供一个简单的分配额外内存的机制

内存保护

提供独立的地址空间使得区分不同进程的私有内存变得容易,地址翻译机制可以以一种自然的方式扩展到提供更好的访问控制

如果一条指令违反了许可条件,CPU就触发一个一般保护故障,将控制传递给内核中异常处理程序。Linux Shell一般将这种异常称为“段错误(Segmentation fault)”

Linux虚拟内存系统

Linux为每个进程维护了一个单独的虚拟地址空间

iOS内存机制

Memory in Use = memoryPage * pageSize

iOS中内存页类型

  • clean memory

    • 可以被移出内存的数据
    • 内存映射文件
    • Frameworks一部分,如__DATA_CONST段
  • dirty memory

    • app修改的内存
    • 所有堆内存分配
    • 解码后的图片缓存区
    • Frameworks中__DATA_DIRTY部分
  • compressed memory

    • 没有传统内存交换
    • 压缩用不到的内存页
    • 当需要使用的时候解压

Virtual memory = clean memory + dirty memory

phys_footprint = dirty memory + compressed memory

Footprint限制

  • 不同设备不同
  • App通常有较高的footprint内存限制
  • Extensions有比较低的内存限制

内存超出限制的异常类型

  • EXC_RESOURCE_EXCEPTION

Resident memory = dirty memory + clean memory loaded in phisical

内存管理方式

  1. Purgeable Memory

Cocoa框架也提供了NSPurgeableData类来帮助确保不会使用过多内存,NSPurgeableData类遵循NSDiscardableContent协议,这个协议的作用是如果访问这个类的实例的对象结束了访问可以释放这个类的实例的内存。你在创建具有一次性子组件的对象时,需要实现NSDiscardableContent。另外NSPurgeableData类使用时跟NSCache没有关联,你可以独立使用

  1. Memory Compression

操作系统在PC端一般会有内存交换机制,但是移动端受限于电量和闪存成本,iOS中对应与内存交换的是一种内存压缩机制

  1. APP nap

如果一个app没有正执行例如更新屏幕内容等用户初始化的工作,播放音乐或者下载文件,系统可以把APP放到APP Nap中。APP Nap通过调节应用程序的CPU使用率和降低计时器的触发频率来节省电池寿命。

  1. Memory pressure warnings

在iOS系统中有几种不同等级的memory pressure warning,从“normal”到“critical”,包括Jetsam机制

  1. Low memory notifications

UIKit使用了几种方式来接收地内存通知

  • 在application delegate里实现applicationDidReceiveMemoryWarning:方法
  • 在你自定义UIViewController的子类里重写didReceiveMemoryWarning方法
  • 注册UIApplicationDidReceiveMemoryWarningNotification对应的通知

内存管理

内存映射

通过将一个虚拟内存区域与一个磁盘上的对象(object)关联起来,以初始化这个虚拟内存区域的内容,这个过程称为内存映射(memory mapping)

除了系统使用内存映射共享一些对象和区域外,也提供了用户级的mmap函数来创建新的虚拟内存区域,并将对象映射到这些区域

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

动态内存分配

动态内存分配器维护着一个进程的虚拟内存区域,称为堆(heap)。分配器将堆视为一组不同大小的块(block)的集合来维护。每个块就是一个连续的虚拟内存片(chunk),要么是已分配的,要么是空闲的。

c标准库提供了一个称为malloc程序包的分配器。

#include <stdlib.h>

void *malloc(size_t size);

垃圾收集

垃圾收集器将内存视为一张有向可达图(reachability graph),该图的节点被分成一组根节点(root node)和一组堆节点(heap node)。

当存在一条从任意根节点出发并到达某节点的有向路径时,我们说该节点是可达的。在任何时刻,不可达节点是垃圾,不能被再次使用。垃圾收集器的角色是维护可达图的某种表示,并通过释放不可达节点且将它们返回给空闲链表。

像ML和Java这样的语言的垃圾收集器,对应用如何创建和使用指针有很严格的控制,能够维护可达图的一种精确的表示,因此可以回收所有垃圾。

C++这样的语言的收集器通常不能维持可达图的精确表示,这样的收集器也叫做保守的垃圾收集器(conservative garbage collector)

iOS中是使用自动引用计数(Automatic Reference Counting)来清理垃圾。

GC

Java

ARC

每次当你创建一个类的实例的时候,ARC分配一个片的内存用于存储对应实例的信息。这片区域存储了实例的类型和跟实例关联的属性。

另外当一个实例不再需要的时候,ARC释放被实例使用的内存,之后内存可以用作其他用途。这确保了实例不被用到的时候不会占用内存空间。

循环引用

如果有2个对象相互对对象持有强引用,这样2个对象都可以保持存活,这被称作循环引用。

内存泄漏

内存泄露包括了OC和Swift语言层面的循环引用,也包括了C,C++等语言和库创建对象后未释放等原因。

内存泄露检测工具

iOS中常见的内存泄露检测工具如下

  • MLeaksFinder

  • FBRetainedCycle

    • NSObject
    • Block
    • Timer
    • Associated Objec

引用

《深入理解计算机系统》

iOS Memory Deep Dive

Memory Usage Performance Guidelines

Improving Your App's Performance