iOS之内核GNU-OOM

4,254 阅读5分钟

什么是OOM

out-of-memory 内存超过限制,iOS的Jetsam机制造成的一种Crash,这种另类Crash,通过通过Signal捕获等方法无法捕获

什么又是FOOM

Foreground-out-of-memory App在前台因消耗内存过多而引起的系统强杀

什么是JetSam机制

JetSam机制是操作系统为了控制内存资源过度所建立的一种管理机制,JetSam是一个独立的进程,每一个进程都会有一个阈值,一旦超过这个值,JetSam就会杀死这个进程,设备的内存是有限制的,并不是无限大的,所以内存资源非常重要。系统进程及用户使用的其他app的进程都会争抢这个资源。由于iOS不支持交换空间,一旦触发低内存事件,Jetsam就会尽可能多的释放应用占用的内存,这样在iOS系统上出现系统内存不足时,应用就会被系统终止。

交换空间

物理内存不够使用该怎么办呢?像一些桌面操作系统,会有内存交换空间,在window上称为虚拟内存。它的机制是,在需要时能将物理内存中的一部分交换到硬盘上去,利用硬盘空间扩展内存空间。

iOS不支持交换空间

但iOS并不支持交换空间,大多数移动设备都不支持交换空间。移动设备的大容量存储器通常是闪存,它的读写速度远远小于电脑所使用的硬盘,这就导致在移动设备上就算使用了交换空间,也并不能提升性能。其次,移动设备的容量本身就经常短缺、内存的读写寿命也有限,所以不适合拿闪存来做内存交换

典型app内存类型

当内存不足的时候,系统会按照一定策略来腾出更多空间供使用,比较常见的做法是将一部分低优先级的数据挪到磁盘上,这个操作称为Page Out。之后当再次访问到这块数据的时候,系统会负责将它重新搬回内存空间中,这个操作称为Page In。

Clean Memory

Clean Memory是指那些可以用以Page Out的内存,只读的内存映射文件,或者是App所用到的frameworks。每个frameworks都有_DATA_CONST段,通常他们都是Clean的,但如果用runtime进行swizzling,那么他们就会变Dirty。

Dirty Memory

Dirty Memory是指那些被App写入过数据的内存,包括所有堆区的对象、图像解码缓冲区,同时,类似Clean memory,也包括App所用到的frameworks。每个framework都会有_DATA段和_DATA_DIRTY段,它们的内存是Dirty的。

值得注意的是,在使用framework的过程中会产生Dirty Memory,使用单例或者全局初始化方法是减少Dirty Memory不错的方法,因为单例一旦创建就不会销毁,全局初始化方法会在类加载时执行。

Compressed Memory

由于闪存容量和读写寿命的限制,iOS 上没有交换空间机制,取而代之使用Compressed memory。

Compressed memory是在内存紧张时能够将最近使用过的内存占用压缩至原有大小的一半以下,并且能够在需要时解压复用。它在节省内存的同时提高了系统的响应速度,特点总结起来如下:

Shrinks memory usage 减少了不活跃内存占用 Improves power efficiency 改善电源效率,通过压缩减少磁盘IO带来的损耗 Minimizes CPU usage 压缩/解压十分迅速,能够尽可能减少 CPU 的时间开销 Is multicore aware 支持多核操作 例如,当我们使用Dictionary去缓存数据的时候,假设现在已经使用了3页内存,当不访问的时候可能会被压缩为1页,再次使用到时候又会解压成3页。

本质上,Compressed memory也是Dirty memory
因此, memory footprint = dirty size + compressed size ,这也就是我们需要并且能够尝试去减少的内存占用。

出现OOM前一定会出现Memory Warning么?

答案是不一定,有可能瞬间申请了大量内存,而恰好此时主线程在忙于其他事情,导致可能没有经历过Memory Warning就发生了OOM。当然即便出现了多次Memory Warning后,也不见得会在最后一次Memory Warning的几秒钟后出现OOM。之前做extension开发的时候,就经常会出现Memory Warnning,但是不会出现OOM,再操作一两分钟后,才出现OOM,而在这一两分钟内,没有再出现过Memory Warning。

当然在内存警告时,处理内存,可以在一定程度上避免出现OOM。

开始分析系统的JetSam

bsd_init当中找到初始化JetSam

// iOS上独有的特性,内存和进程的休眠的常驻监控线程
#if CONFIG_FREEZE   // 这个宏是内核会对进程进行冷冻而不是kill掉
#ifndef CONFIG_MEMORYSTATUS
    #error "CONFIG_FREEZE defined without matching CONFIG_MEMORYSTATUS"
#endif
    /* Initialise background freezing */
    bsd_init_kprintf("calling memorystatus_freeze_init\n");
    memorystatus_freeze_init(); //从内核中开启优先级最高的线程来监控整个系统的内存情况
#endif

//iOS独有,JetSAM(即低内存事件的常驻监控线程)
#if CONFIG_MEMORYSTATUS
    /* Initialize kernel memory status notifications */
    bsd_init_kprintf("calling memorystatus_init\n");
    memorystatus_init(); //从内核中开启优先级最高的线程来监控整个系统的内存情况
#endif /* CONFIG_MEMORYSTATUS */

优先级

在内核里面所有的进程都有一个优先级,通过一个数组维护,数组的每一项是一个进程的列表也就是memstat_bucket。这个数组的大小则是JETSAM_PRIORITY_MAX + 1

#define MEMSTAT_BUCKET_COUNT (JETSAM_PRIORITY_MAX + 1)

typedef struct memstat_bucket {
    TAILQ_HEAD(, proc) list; //一个TAILQ_HEAD的双向链表用来存放优先级
    int count; //进程个数
    int relaunch_high_count;
} memstat_bucket_t;

extern memstat_bucket_t memstat_bucket[MEMSTAT_BUCKET_COUNT]; //优先级队列(里面包含不同优先级的结构)    

在这里可以看到优先级数字,其中SpringBoardJETSAM_PRIORITY_HOME,后台应用程序为JETSAM_PRIORITY_BACKGROUND

imagepng

回到 memorystatus_init

jetsam_threads = zalloc_permanent(sizeof(struct jetsam_thread_state) *

max_jetsam_threads, ZALIGN(struct jetsam_thread_state));


/* Initialize all the jetsam threads */

for (i = 0; i < max_jetsam_threads; i++) {

// max_jetsam_threads 性能差为1个 ,普通为3个
jetsam_threads[i].inited = FALSE;
jetsam_threads[i].index = i;


//这些线程的优先级是内核所能分配的最高级95 MAXPRI_KERNEL  (XNU的线程优先级为0-127)
result = kernel_thread_start_priority(memorystatus_thread, NULL, 95 /* MAXPRI_KERNEL */, &jetsam_threads[i].thread);
if (result != KERN_SUCCESS) {
     panic("Could not create memorystatus_thread %d", i);
}
     thread_deallocate(jetsam_threads[i].thread);
     }
}

来到 memorystatus_thread

系统中会有一个线程专门来管理内存状态,当内存出现问题或者压力过大时,将会通过一些方法来干掉APP回收内存

imagepng

memorystatus_action_needed()是触发OOM的核心判断条件。

imagepng

我们来到 is_reason_thrashing

imagepng

memorystatus_action_needed为true来到了Highwater

来到这里证明当前内存紧张,来到memorystatus_act_on_hiwat_processes

memorystatus_act_on_hiwat_processes

imagepng

则会调用memorystatus_kill_hiwat_proc

  1. 优先级队列里面取出优先级最低的进程
  2. while循环查找进程的内存是否高于阈值
  3. 如果高于则通过memorystatus_do_kill杀掉这个进程,并结束循环

imagepng

如果经过了Highwater还是没有办法结束进程,将来到memorystatus_act_aggressive

imagepng

memorystatus_act_aggressive 也就是我们所经常遇到的OOM

这部分代码太多 我就贴关键的代码了

如果上面memorystatus_act_aggressive函数没有杀死任何进程,那么就需要通过LRU来杀死Jetsam队列中的第一个进程

总结 - 系统触发OOM的过程

  1. JetSam线程初始化完毕,从外部接受到有内存压力
  2. 如果接收到的内存压力是当前物理内存达到限制时,则触发per-process-limit类型的OOM,然后退出流程
  3. 如果接收到内存压力是其他类型时,唤醒JetSam线程,判断当前可用内存紧张则进入OOM逻辑
  4. 首先遍历优先级最低的进程,判断进程是否高于阈值,如果没有高于阈值,则查找比当前优先级高一级的进程,直到找到后,触发high-water类型OOM
  5. 如果没有触发high-water ,那就先回收一个优先级较低的进程或者标记为随时可回收的进程
  6. 当所有低优先级进程和随时可回收的进程都被杀掉后,如果memorystatus_available_pages可用内存依然低于正常水平,那就开始杀掉后台进程,每杀掉一个后台进程,则判断一下memorystatus_available_pages可用内存是否还是低于正常水平,如果已经恢复到正常,则挂起线程,等待唤醒
  7. 当所有后台进程都被杀死后,可用内存还是低于正常,那就开始杀掉前台的进程,挂起线程,等待唤醒
  8. 如果上面没有杀掉任何进程,就通过LRU杀死JetSam队列中的第一个进程,挂起线程,等待唤醒

Matrix如何检测OOM

Matrix采用的是排除法,在每次启动Matrix的时候都会调用[MatrixAppRebootAnalyzer checkRebootType];

来看一下checkRebootType方法中有什么

+ (void)checkRebootType {
    if ([MatrixDeviceInfo isBeingDebugged]) { // 是否在DEBUG
        MatrixInfo(@"app is being debugged");
        MatrixAppRebootInfo *info = [MatrixAppRebootInfo sharedInstance];
        info.isAppCrashed = NO;
        info.isAppQuitByExit = NO;
        info.isAppQuitByUser = NO;
        info.isAppWillSuspend = NO;
        info.isAppEnterBackground = NO;
        info.isAppEnterForeground = NO;
        info.isAppBackgroundFetch = NO;
        info.isAppSuspendKilled = NO;
        info.isAppMainThreadBlocked = NO;
        info.dumpFileName = @"";
        info.userScene = @"";
        [info saveInfo];
        return;
    }

    MatrixAppRebootInfo *info = [MatrixAppRebootInfo sharedInstance];

    if (info.isAppCrashed) {
        // App是否发生了普通的Crash
        s_rebootType = MatrixAppRebootTypeNormalCrash;
    } else if (info.isAppQuitByUser) {
        // 是否用户主动退出应用
        s_rebootType = MatrixAppRebootTypeQuitByUser;
    } else if (info.isAppQuitByExit) {
        // 是否调用了exit相关的函数
        s_rebootType = MatrixAppRebootTypeQuitByExit;
    } else if (info.isAppWillSuspend || info.isAppBackgroundFetch) {
        // App是否挂起Suspend或者执行了BackgroundFetch
        if (info.isAppSuspendKilled) {
            s_rebootType = MatrixAppRebootTypeAppSuspendCrash;
        } else {
            s_rebootType = MatrixAppRebootTypeAppSuspendOOM;
        }
    } else if ([MatrixAppRebootAnalyzer isAppChange]) {
        // App的版本是否发生了改变
        s_rebootType = MatrixAppRebootTypeAPPVersionChange;
    } else if ([MatrixAppRebootAnalyzer isOSChange]) {
        // 手机系统是否升级了
        s_rebootType = MatrixAppRebootTypeOSVersionChange;
    } else if ([MatrixAppRebootAnalyzer isOSReboot]) {
        // 手机是否重启了
        s_rebootType = MatrixAppRebootTypeOSReboot;
    } else if (info.isAppEnterBackground) {
        // App是否处于后台
        s_rebootType = MatrixAppRebootTypeAppBackgroundOOM;
    } else if (info.isAppEnterForeground) {
        // App是否处于前台
        if (info.isAppMainThreadBlocked) {
            // 主线程是否卡死了 
            s_rebootType = MatrixAppRebootTypeAppForegroundDeadLoop;
            s_lastDumpFileName = info.dumpFileName;
        } else {
            // 触发Foreground OOM
            s_rebootType = MatrixAppRebootTypeAppForegroundOOM;
            s_lastDumpFileName = @"";
        }
    } else {
        s_rebootType = MatrixAppRebootTypeOtherReason;
    }

现在所遇到的OOM主要都是FOOM,因为优先级的缘故,App在后台的时候,即使占用内存很少,也有可能被前台应用过多占用内存而被杀死,所以关注的点上还是要在FOOM

参考资料

深入了解iOS中的OOM(低内存崩溃)

(你真的了解OOM吗?——京东iOS APP内存优化实录)

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

iOS微信内存监控