iOS OOM

  1. 技术点:
  2. iOS内存分配系统的实现(用户层级)
    1. 虚拟内存分配相关的知识:
    2. 物理内存: pmap…
  3. iOS abort 机制
    1. Pageout
    2. Jetsam
      1. 杀的类别
      2. 触发入口
    3. watchdog
    4. 如何抓到回调与如何监控
    5. 数据结构设计
    6. OOM事件捕获

最近在做oom,简单的总结下相关知识

Out Of Memory 是 Jetsam同过响应压力通知杀掉优先级内消耗内存太多的进程, 导致应用闪退的一种现象,难于捕获和分析。

技术点:

1. iOS 内存分配系统的实现
2. 如何抓到回调与如何监控
3. 数据结构设计
4. iOS abort机制
5. OOM事件捕获

iOS内存分配系统的实现(用户层级)

iOS中,内存堆分配的基本控制者是 malloc_zone, 通常是default_zone. 实际上是一个 scallable zone. 我们的通过malloc 分配的内存都是 通过Zone中的空闲内存块链表中获取的. 实际上Zone 从VMPage获取4k对齐的内存(mvm_allocate_pages). 然后再分给我们小块的,效率高,我们通过malloc分配内存,就不必每次都需要申请vmpages. malloc 内存申请分为 tiny, small, large…

虚拟内存分配相关的知识:

vm_map, vm_map_entry, vm_object, vm_page, vm_object, vm_page
对虚拟内存页面的调用都在bsd/kern/kern_mman.c中实现。

物理内存: pmap…

iOS abort 机制

Pageout

管理页面交换的策略,判断哪些页面需要写回到其后备存储。
垃圾回收线程 (vm_pageout_grabage_collect()) 调用 consider_pressure_events -> vm_dispatch_memory_pressure() -> BSD -> NOTE_VM_PRESSURE-> 响应压力通知
如果进程并不是总能找到可以抛弃的内存,当这种协作方法失败时,Jetsam机制介入。

Jetsam

通过响应压力通知杀掉优先级内消耗内存太多的进程。
BSD层起了一个内核优先级最高的线程VM_memorystatus,这个线程会在维护两个列表,一个是我们之前提到的基于进程优先级的进程列表,还有一个是所谓的内存快照列表,即保存了每个进程消耗的内存页memorystatus_jetsam_snapshot。
这个常驻线程接受从内核对于内存的守护程序pageout通过内核调用给每个App进程发送的内存压力通知,来处理事件,这个事件转发成上层的UI事件就是平常我们会收到的全局内存警告或者每个ViewController里面的didReceiveMemoryWarning。

杀的类别

读了一下源码,发现 杀的机制有如下两种,他们大致的执行流程如下

highwater 的处理 -> 我们App占用的内存不要超过限制

1. 从优先级列表里循环寻找线程
2. 判断是否满足p_memstat_memlimit的限制条件
3. DiagnoseActive, FREEZE过滤
4. 杀进程,杀到了exit, 否则继续循环

memorystatus_act_aggressive处理 -> 内存占用高按优先级杀

1. 根据policy加载 jld_bucket_count, 用来判断是否开杀
2. 从JETSAM_PRIORITY_ELEVATED_INACTIVE 开始杀
3. jld_bucket_count 和 memorystatus_jld_eval_period_msecs 判断是否开杀
4. 根据优先级从低向高杀,直到memorystatus_avail_pages_below_pressure

触发入口

1
2
3
4
5
6
7
8
9
10
11
12
# static boolean_t
memorystatus_action_needed(void)
{
#if CONFIG_EMBEDDED
return (is_reason_thrashing(kill_under_pressure_cause) ||
is_reason_zone_map_exhaustion(kill_under_pressure_cause) ||
memorystatus_available_pages <= memorystatus_available_pages_pressure);
#else /* CONFIG_EMBEDDED */
return (is_reason_thrashing(kill_under_pressure_cause) ||
is_reason_zone_map_exhaustion(kill_under_pressure_cause));
#endif /* CONFIG_EMBEDDED */
}

针对这种情况我们可以

memorystatus_action_needed 判断 -> 规避Jetsam处理

1. 是否因为thrashing(如果是EMBEDED则不会触发这种情况)
2. 是否因为zone_map_exhaustion(判断Zone的消耗情况, vm_map 相关参数来做,感觉这个意义不大)
3. 是否因为memorystatus_available_pages <= memorystatus_available_pages_pressure(是否是EMBEDDED)(根据物理内存page占比计算得到) -> boot_arguments...能取到(外部拿不到,只能走内核调试拿.. )

watchdog

为了避免应用陷入错误状态导致界面无响应,Apple 设计了看门狗 (WatchDog) 机制。一旦超时,强制杀死进程。在不同的生命周期,触发看门狗机制的超时时间有所不同:

生命周期 超时时间
启动 Launch 20 s
恢复 Resume 10 s
悬挂 Suspend 10 s
退出 Quit 6 s
后台 Background 10 min

如何抓到回调与如何监控

抓到:libmalloc 中的 malloc_logger 函数指针。通过这个可以抓到所有malloc类分配。
vm 则可以根据hook或者私有变量,和 malloc_logger一致。

监控: 每次抓到都获取调用栈,存储进自己定义的数据结构中。 这里为什么不会循环调用需要值得注意一下。是通过不同的逻辑分支,保证不会走到相同的带分配的逻辑分支。

数据结构设计

1. 空间占用
2. 访问速度
3. 细节存储优化

OOM事件捕获

  1. 现有方案,排除法
  2. 存在的问题 applicationstate不准等
  3. 经验值+ANR 优化
script>