OOM问题小记

纸上得来终觉浅,绝知此事要躬行

一、概述

1、OOM是什么
  • OOM,即Out of Memory;是由于iOS的Jetsam机制导致的奔溃,不同于常规Crash,通过 Signal等Crash等监控方案是无法捕获到OOM事件。

  • 造成OOM的原因可能有两个:系统由于整体内存使用较高,系统基于优先级杀死优先级较低的 App;当前 App 达到了 “high water mark”,也就是达到了系统对单个 App 的内存限制,当前App被系统强杀。

2、Jetsam机制是什么
  • Jetsam机制指的是操作系统为了控制内存资源过度使用而采用的一种资源管控机制。
  • 由于iOS设备不存在交换区导致的内存受限,iOS内核不得不把一些优先级不高或者占用内存过大的App杀掉;在杀掉App后会记录一些数据信息并保存到日志。
  • Jetsam产生的这些日志可以在手机设置->隐私->分析中找到,日志是以JetsamEvent开头,日志中有内存页大小(pageSize),CPU时间(cpuTime)等字段。

二、获取App的内存使用上限

1、通过JetsamEvent 日志计算内存限制值
  • 查看设置->隐私->分析中以JetsamEvent开头的系统日志,关注两个重要的信息
1
2
//内存页大小(字节数)(16384Byte = 16KB)
"pageSize" : 16384,
1
2
3
//内存页达到上限
"rpages" : 948, //App 占用的内存页数量
"reason" : "per-process-limit", //App 占用的内存超过了系统对单个 App 的内存限制。
  • 该App内存限制上限:pageSize rpages = 16384 948 /1024/1014 = 14.8MB
  • 【有人可能会有疑问】某App内存使用上限只有区区不到15MB,不太可能吧。其实这是正常的,Jetsam机制会把优先级不高或内存使用过大的App强杀掉,这个App属于优先级不高,系统将其强杀,为优先级高的App提供更多内存资源。
  • App优先级可以这么理解:前台App > 后台App; 占用内存少 > 占用内存多
  • JetsamEvent日志属于系统级别的,是在系统目录下的。App开发者没有权限获得系统目录下内容。只能连接Xcode获取 或 手动进入手机设置->隐私->分析中找到日志并分享出来。
2、收到内存警告通知时获取当前App的内存使用值
  • iOS系统会开启优先级最高的线程 vm_pressure_monitor来监控系统的内存压力情况,并通过一个堆栈来维护所有App的进程。
  • 当监控系统内存的线程发现某App内存有压力了,就发出通知,收到通知的App执行对应的处理方法,在这里可以编写释放内存的逻辑,就可能避免App被强杀。
  • 在收到内存警告通知时,获取当前 App的内存使用值,能获得App内存使用阈值的近似值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//获取当前App的内存使用值
uint64_t qs_getAppMemoryBytes() {
task_vm_info_data_t vmInfo;
mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
kern_return_t result = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
if (result != KERN_SUCCESS)
return 0;
return vmInfo.phys_footprint;
}

//注册监听内存警告通知
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(handleReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

//收到内存警告通知通知的处理
- (void)handleReceiveMemoryWarning:(NSNotification *)notification {
//
NSLog(@"handleReceiveMemoryWarning");
NSInteger appMemoryBytes = qs_getAppMemoryBytes();
//....
}

说明:可以通过模拟器的Debug -> Simulate Memory Warning 模拟内存警告

3、通过XNU获取内存限制值

在XNU中,可以通过memorystatus_priority_entry 这个结构体,得到进程的优先级和内存限制值。结构体如下:

1
2
3
4
5
6
7
8
// 获取进程的 pid、优先级、状态、内存阈值等信息
typedef struct memorystatus_priority_entry {
pid_t pid;
int32_t priority; //线程优先级
uint64_t user_data;
int32_t limit; //进程内存限制值
uint32_t state;
} memorystatus_priority_entry_t;

说明:通过XNU获取内存限制值需要root权限,而App的权限不够,所以要获得App的内存限制需要到到越狱设备上。

4、总结

总的来说,获取App的内存使用阈值,有三种方式,三种方式的对比如下:

  • 通过JetsamEvent 日志计算内存限制值 局限大,不适合线上使用;
  • 收到内存警告通知时获取当前App的内存使用值,这个获取的是上限的近似值,虽然不很准确,但是适合线上使用;
  • 通过XNU获取内存限制值,局限更大,虽然能知道各进程之前的优先级,内存阈值,但是需要设备越狱,更不适合线上使用。

——————————————————————————————————————————————————

很多东西后续完善,现在随便写写, 尴尬

三、内存分配获取

1、概述
  • OOM的排查思路:监控App使用内存增长,在收到内存警告通知时,dump 内存信息,获取对象名称、对象个数、各对象的内存值,并在合适的时机上报到服务器。
2、内存分配函数
  • 内存分配函数malloc和calloc等默认使用的是nano_zone,no_zone 是 256B 以下小内存的分配,大于 256B 的时候会使用 scalable_zone 来分配。
  • 主要针对大内存的分配监控,所以只针对scalable_zone进行分析,可以过滤掉很多小内存分配,比如malloc 函数用的是 malloc_zone_malloc,calloc用的是malloc_zone_calloc。
  • 使用scalable_zone分配内存的函数都会用到malloc_logger函数,它可以统计并管理内存的分配情况。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void *malloc_zone_malloc(malloc_zone_t *zone, size_t size)
{
MALLOC_TRACE(TRACE_malloc | DBG_FUNC_START, (uintptr_t)zone, size, 0, 0);
void *ptr;
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
if (size > MALLOC_ABSOLUTE_MAX_SIZE) {
return NULL;
}
ptr = zone->malloc(zone, size);
// 在 zone 分配完内存后就开始使用 malloc_logger 进行进行记录
if (malloc_logger) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE, (uintptr_t)zone, (uintptr_t)size, 0, (uintptr_t)ptr, 0);
}
MALLOC_TRACE(TRACE_malloc | DBG_FUNC_END, (uintptr_t)zone, size, (uintptr_t)ptr, 0);
return ptr;
}

说明:利用fishhook去hook malloc_logger函数,可以掌握内存的分配情况,当出现问题时,将内存分配日志捞上来,可以跟踪到内存不合理增大的原因。

3、阅读

iOS微信内存监控

深入理解内存分配

iOS Out-Of-Memory 原理阐述及方案调研

OOMDetector

iOS爆内存问题解决方案-OOMDetector组件

文章作者: 南华coder
文章链接: http://buaa0300/nanhuacoder.com/2019/04/17/iOS-OOM/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 南华coder的空间