Learn more about oom (low memory crash) in iOS

Posted by drums on Thu, 16 Jan 2020 10:18:18 +0100

In the iOS development process or user feedback, you may often see this situation. When you use it, you will crash. When you check the crash stack in the background, you cannot find the crash log. In fact, most of this may be due to the system's low memory crash, that is, oom (another possibility is that the main thread is stuck, causing watchdog to kill the application). The log of low memory crash usually starts with JetsamEvent, and the log contains fields such as page size, CPU time, etc.

What is OOM?

What is OOM? It's the abbreviation of out of memory, which literally means that the memory exceeds the limit. It is a kind of "alternative" Crash caused by the Jetsam mechanism of iOS. It is different from the conventional Crash, and cannot capture OOM events through the Crash monitoring schemes such as Signal capture.

Of course, there will be words like FOOM, which stands for foregroup out of memory, which means that the app consumes too much memory in the foreground and causes the system to kill. This is what this article will discuss. OOM in the background is not necessarily caused by the app itself. Most of it is because the current app in the foreground occupies too much memory. In order to ensure the normal operation of the foreground application, the system cleans up the background application.

What is Jetsam mechanism

Jetsam mechanism can be understood as a kind of management mechanism adopted by the operating system to control the excessive use of memory resources. Jetsam is an independent running process, each process has a memory threshold, once the threshold is exceeded, jetsam will kill the process immediately.

Why design Jetsam mechanism

First of all, the memory of the device is limited, not unlimited, so memory resources are very important. The system process and other app processes used by users will compete for this resource. As iOS does not support swap space, Jetsam will release as much memory as possible once low memory events are triggered, so that when there is insufficient system memory on iOS system, the application will be terminated by the system.

Swap space

What should I do if I don't have enough physical memory? Like some desktop operating systems, there will be memory exchange space, which is called virtual memory on window s. Its mechanism is to exchange part of the physical memory to the hard disk when necessary, and expand the memory space by using the hard disk space.

iOS does not support swap space

However, iOS does not support swap space. Most mobile devices do not support swap space. The large capacity memory of mobile devices is usually flash memory, its read-write speed is far less than the hard disk used by the computer, which results in that even if the exchange space is used on the mobile devices, it can not improve the performance. Secondly, the capacity of the mobile device itself is often in short supply, and the read-write life of the memory is also limited, so in this case, it is a bit luxurious to use flash memory for memory exchange.

It should be noted that there are few articles on the Internet saying that iOS does not have a virtual memory mechanism, which actually means that iOS does not have a swap space mechanism.

Typical app memory type

When the memory is insufficient, the system will make more space for use according to certain strategies. A common practice is to move some low priority data to disk. This operation is called Page Out. Later, when the data is accessed again, the system will be responsible for moving it back to the memory space. This operation is called Page In.

Clean Memory

Clean Memory refers to the memory that can be used for Page Out, read-only memory mapping files, or frameworks used by App. Every framework has "data" const segments, usually they are clean, but if swizzling with runtime, they will become Dirty.

Dirty Memory

Dirty Memory refers to the memory written to DATA by App, including all heap objects and image decoding buffers. Meanwhile, similar to Clean memory, it also includes frameworks used by App. Each framework will have "DATA" and "DATA" segments, whose memory is dirty.

It is worth noting that Dirty Memory will be generated in the process of using framework. Using single instance or global initialization method is a good way to reduce Dirty Memory, because once a single instance is created, it will not be destroyed, and the global initialization method will be executed when the class is loaded.

Compressed Memory

Due to the limitation of flash memory capacity and read-write life, there is no swap space mechanism on iOS, so Compressed memory is used instead.

Compressed memory is able to compress the recently used memory usage to less than half of the original size when memory is tight, and can decompress and reuse when needed. It not only saves memory but also improves the response speed of the system. The characteristics are summarized as follows:

  • Shrinks memory usage reduces inactive memory usage
  • Improve power efficiency, reduce disk IO loss through compression
  • Minimizes CPU usage compresses / decompresses quickly, minimizing CPU time overhead
  • Is multicore aware supports multicore operations

For example, when we use Dictionary to cache data, suppose that now we have used 3 pages of memory, when we do not access it, it may be compressed to 1 page, and when we use it again, it will be decompressed to 3 pages.

In essence, Compressed memory is also Dirty memory.
Therefore, memory footprint = dirty size + compressed size, which is what we need and can try to reduce memory consumption.

Memory Warning

I believe that it is not new to MemoryWarning. Every UIViewController has a didReceivedMemoryWarning method.

When the use of memory is a little bit up, rather than a burst of memory directly. Before reaching the memory critical point, the system will issue a memory warning to each running application, telling the app to clean up its memory. The memory warning is not always caused by its own app.

Memory compression technology makes it complicated to free memory. Memory compression technology is implemented at the operating system level, which is insensitive to processes. Interestingly, if the current process receives a memory warning, the process is ready to release a large amount of misused memory at this time. If too much compressed memory is accessed, the memory pressure will be greater when the memory is decompressed, and then OOM will appear, which will be killed by the system.

The purpose of caching data is to reduce the pressure of CPU, but too much cache will occupy too much memory. In some scenarios where data needs to be cached, NSCache can be used instead of NSDictionary. The memory allocated by NSCache is actually Purgeable Memory, which can be automatically released by the system. In the book Effective Objective 2.0, it is also recommended that the combination of NSCache and nspurabledata can not only enable the system to reclaim memory according to the situation, but also remove related objects at the same time of memory cleaning.

Will Memory Warning appear before OOM? The answer is not necessarily. It is possible that a large amount of memory is applied for in an instant, and the main thread is busy with other things at this time, which may lead to OOM without Memory Warning. Of course, even if there are many memory warnings, it is not likely that OOM will appear a few seconds after the last Memory Warning. Before extension development, Memory Warning often appeared, but there would be no OOM. After another one or two minutes of operation, there would be OOM. In this one or two minutes, there was no Memory Warning.

Of course, when dealing with memory warning, OO can be avoided to some extent.

How to determine the threshold of OOM

Experienced students must know that the threshold value of OOM for different devices is different. So how do we know the threshold of OOM?

Method 1

When our App is killed by Jetsam mechanism, the system log will be generated in the mobile phone. In the mobile phone system settings privacy analysis, you can get the log at the beginning of JetSamEvent. In these logs, you can get some memory information about the App, such as the iPhone 8 (IOS 11.4.1) I currently use. In the front part of the log, I saw the pageSize, and I found the per process limit item (not all logs have it, but you can find it). I can get the threshold value of OOM by using the rpages * pageSize of the item.

{"bug_type":"298","timestamp":"2020-01-03 04:11:13.65 +0800","os_version":"iPhone OS 11.4.1 (15G77)","incident_id":"2723B2EA-7FB8-49A6-B2FC-49F10C748D8A"}
{
  "crashReporterKey" : "a6ad027ba01b1e66d0b3d8446aaef5dbd75dd732",
  "kernel" : "Darwin Kernel Version 17.7.0: Mon Jun 11 19:06:27 PDT 2018; root:xnu-4570.70.24~3\/RELEASE_ARM64_T8015",
  "product" : "iPhone10,1",
  "incident" : "2723B2EA-7FB8-49A6-B2FC-49F10C748D8A",
  "date" : "2020-01-03 04:11:13.65 +0800",
  "build" : "iPhone OS 11.4.1 (15G77)",
  "timeDelta" : 4,
  "memoryStatus" : {
  "compressorSize" : 39010,
  "compressions" : 2282594,
  "decompressions" : 1071238,
  "zoneMapCap" : 402653184,
  "largestZone" : "APFS_4K_OBJS",
  "largestZoneSize" : 35962880,
  "pageSize" : 16384,
  "uncompressed" : 105360,
  "zoneMapSize" : 118865920,
  "memoryPages" : {
    "active" : 39800,
    "throttled" : 0,
    "fileBacked" : 28778,
    "wired" : 19947,
    "anonymous" : 32084,
    "purgeable" : 543,
    "inactive" : 19877,
    "free" : 2935,
    "speculative" : 1185
  }
},
...
  {
    "uuid" : "a2f9f2db-a110-3896-a0ec-d82c156055ed",
    "states" : [
      "frontmost",
      "resume"
    ],
    "killDelta" : 11351,
    "genCount" : 0,
    "age" : 361742447,
    "purgeable" : 0,
    "fds" : 50,
    "coalition" : 2694,
    "rpages" : 89600,
    "reason" : "per-process-limit",
    "pid" : 2541,
    "cpuTime" : 1.65848,
    "name" : "MemoryTest",
    "lifetimeMax" : 24126
  },
...

The current memory test threshold is 16384 * 89600 / 1024 / 1024 = 1400MB.

Method 2

At present, there are many people on the network sorting out the OOM memory corresponding table. I prefer this version according to the actual situation.

I created one more list by sorting Jaspers list by device RAM (I made my own tests with Split's tool and fixed some results - check my comments in Jaspers thread).

device RAM: percent range to crash

256MB: 49% - 51%
512MB: 53% - 63%
1024MB: 57% - 68%
2048MB: 68% - 69%
3072MB: 63% - 66%
4096MB: 77%
6144MB: 81%

Special cases:

iPhone X (3072MB): 50%
iPhone XS/XS Max (4096MB): 55%
iPhone XR (3072MB): 63%
iPhone 11/11 Pro Max (4096MB): 54% - 55%

Device RAM can be read easily:

[NSProcessInfo processInfo].physicalMemory
From my experience it is safe to use 45% for 1GB devices, 50% for 2/3GB devices and 55% for 4GB devices. Percent for macOS can be a bit bigger.

Method 3

First, we can get the memory occupied by the current application through the method. The code is as follows

- (int)usedSizeOfMemory {
    task_vm_info_data_t taskInfo;
    mach_msg_type_number_t infoCount = TASK_VM_INFO_COUNT;
    kern_return_t kernReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&taskInfo, &infoCount);

    if (kernReturn != KERN_SUCCESS) {
        return 0;
    }
    return (int)(taskInfo.phys_footprint / 1024 / 1024);
}

There are other codes that use taskInfo.resident_size, but the value is not accurate. When I compare Xcode Debug, I find that the taskinfo.phys'footprint value is basically the same as Xcode Debug. In XNU's task.c, we also find how the value is calculated.

/*
 * phys_footprint
 *   Physical footprint: This is the sum of:
 *     + (internal - alternate_accounting)
 *     + (internal_compressed - alternate_accounting_compressed)
 *     + iokit_mapped
 *     + purgeable_nonvolatile
 *     + purgeable_nonvolatile_compressed
 *     + page_table
 */
 Local test:
 On IOS 11, the difference between the values of phys ﹣ footprint and Xcode DEBUG is less than 1M,
 On IOS 13, the values of "phys" and "footprint" are exactly the same as those of Xcode DEBUG.
 Students with obsessive-compulsive disorder can use IOS 11
 ((taskinfo. Internal + taskinfo. Compressed - taskinfo. Purgeable [volatile [PMAP]) instead of phys [footprint].

After we get this value, we can start a thread to apply for 1MB of memory in a loop until the first memory warning and OOM are reached.

#import "ViewController.h"
#import <mach/mach.h>

#define kOneMB  1048576

@interface ViewController ()
{
    NSTimer *timer;

    int allocatedMB;
    Byte *p[10000];
    
    int physicalMemorySizeMB;
    int memoryWarningSizeMB;
    int memoryLimitSizeMB;
    BOOL firstMemoryWarningReceived;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    physicalMemorySizeMB = (int)([[NSProcessInfo processInfo] physicalMemory] / kOneMB);
    firstMemoryWarningReceived = YES;
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
        
    if (firstMemoryWarningReceived == NO) {
        return ;
    }
    memoryWarningSizeMB = [self usedSizeOfMemory];
    firstMemoryWarningReceived = NO;
}

- (IBAction)startTest:(UIButton *)button {
    [timer invalidate];
    timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(allocateMemory) userInfo:nil repeats:YES];
}

- (void)allocateMemory {
    
    p[allocatedMB] = malloc(1048576);
    memset(p[allocatedMB], 0, 1048576);
    allocatedMB += 1;
    
    memoryLimitSizeMB = [self usedSizeOfMemory];
    if (memoryWarningSizeMB && memoryLimitSizeMB) {
        NSLog(@"----- memory warnning:%dMB, memory limit:%dMB", memoryWarningSizeMB, memoryLimitSizeMB);
    }
}

- (int)usedSizeOfMemory {
    task_vm_info_data_t taskInfo;
    mach_msg_type_number_t infoCount = TASK_VM_INFO_COUNT;
    kern_return_t kernReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&taskInfo, &infoCount);

    if (kernReturn != KERN_SUCCESS) {
        return 0;
    }
    return (int)(taskInfo.phys_footprint / kOneMB);
}

@end

In this way, we can debug and view the last log of the console.

2020-01-03 11:52:26.353765+0800 MemoryTest[2561:599014] ----- memory warnning:1289MB, memory limit:1397MB
2020-01-03 11:52:26.363799+0800 MemoryTest[2561:599014] ----- memory warnning:1289MB, memory limit:1398MB
2020-01-03 11:52:26.373895+0800 MemoryTest[2561:599014] ----- memory warnning:1289MB, memory limit:1399MB

We found that the memory warning is 1289MB. If we have a log of 1399MB, it means that the OOM value is 1400MB.

Method 4 (for IOS 13 system)

IOS 13 system os/proc.h provides a new API to view the currently available memory

#import <os/proc.h>

extern size_t os_proc_available_memory(void);

+ (CGFloat)availableSizeOfMemory {
    if (@available(iOS 13.0, *)) {
        return os_proc_available_memory() / 1024.0 / 1024.0;
    }
    // ...
}

With this value, we can calculate the memory limit of the current application. Tested it with an iPhone Xs Max. The memory limit obtained by method 1 is 134278 * 16384 / 1024 / 1024 = 2098M. The memory value obtained by method 3 is 2098M.

- (int)limitSizeOfMemory {
    if (@available(iOS 13.0, *)) {
        task_vm_info_data_t taskInfo;
        mach_msg_type_number_t infoCount = TASK_VM_INFO_COUNT;
        kern_return_t kernReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&taskInfo, &infoCount);

        if (kernReturn != KERN_SUCCESS) {
            return 0;
        }
        return (int)((taskInfo.phys_footprint + os_proc_available_memory()) / 1024.0 / 1024.0);
    }
    return 0;
}

Through this method, the value is 2098M.

The above are several ways to get the values of different applications of OOM. Whether it's application or application extension, it can pass the above methods. However, the memory limit of application expansion is very strict, which is far lower than that of ordinary applications. For example, the application memory limit of the iPhone XS Max is 2098M, while that of the custom keyboard of the same device is 66M (too little).

Source code exploration

We know that the kernel of IOS / Mac OS is XNU, and XNU is open source. We can explore the specific implementation of Apple Jetsam in the open source XNU kernel source code.

The inner layer of XNU kernel is Mach layer. As a microkernel, mach is a thin layer that only provides basic services, such as processor management and scheduling and IPC (interprocess communication). The second major part of XNU is the BSD layer. We can think of it as an outer ring around the Mach layer. BSD provides an interface for the end user's application program. Its responsibilities include process management, file system and network.

The common JetSam time in memory management is also generated by BSD, so let's explore the principle from BSD init as the entry point.

BSD init basically initializes various subsystems, such as virtual memory management and so on.

BSD init

Memory related steps are as follows:

//1. Initialize BSD memory zone, which is based on Mach kernel
kmeminit();

//2.iOS unique features, memory and process sleep resident monitoring thread
#if CONFIG_FREEZE
#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

//3.iOS unique, JetSAM (i.e. resident monitoring thread for low memory events)
#if CONFIG_MEMORYSTATUS
	/* Initialize kernel memory status notifications */
	bsd_init_kprintf("calling memorystatus_init\n");
	memorystatus_init();
#endif /* CONFIG_MEMORYSTATUS */

The two methods of MEMORYSTATUS? Free? Init() and MEMORYSTATUS? Init() call the interface exposed in Kern? MEMORYSTATUS. C. The main function is to open two threads with the highest priority from the kernel to monitor the memory of the whole system.

The function involved in config? Free. When this macro is enabled, the kernel will freeze the process rather than Kill it. The code related to process sleep is not covered in this article.

To return to the topic of iOS OOM crash, we just need to focus on the MEMORYSTATUS? Init () method.

Introduction to knowledge points

  • There is a priority distribution for all processes in the kernel, which is maintained by an array. Each item in the array is a list of processes. The size of this array is jetsam? Priority? MAX + 1.
#define MEMSTAT_BUCKET_COUNT (JETSAM_PRIORITY_MAX + 1)

typedef struct memstat_bucket {
    TAILQ_HEAD(, proc) list;    //  A two-way linked list of tailq'u head is used to store the processes under this priority
    int count;  //  Number of processes
} memstat_bucket_t;

memstat_bucket_t memstat_bucket[MEMSTAT_BUCKET_COUNT];//Priority queue (including structures of different priorities)
  • In Kern? MEMORYSTATUS. H, we can find definitions related to jetsam? Priority? Max value and process priority:
#define JETSAM_PRIORITY_REVISION                  2

#define JETSAM_PRIORITY_IDLE_HEAD                -2
/* The value -1 is an alias to JETSAM_PRIORITY_DEFAULT */
#define JETSAM_PRIORITY_IDLE                      0
#define JETSAM_PRIORITY_IDLE_DEFERRED		  1 /* Keeping this around till all xnu_quick_tests can be moved away from it.*/
#define JETSAM_PRIORITY_AGING_BAND1		  JETSAM_PRIORITY_IDLE_DEFERRED
#define JETSAM_PRIORITY_BACKGROUND_OPPORTUNISTIC  2
#define JETSAM_PRIORITY_AGING_BAND2		  JETSAM_PRIORITY_BACKGROUND_OPPORTUNISTIC
#define JETSAM_PRIORITY_BACKGROUND                3
#define JETSAM_PRIORITY_ELEVATED_INACTIVE	  JETSAM_PRIORITY_BACKGROUND
#define JETSAM_PRIORITY_MAIL                      4
#define JETSAM_PRIORITY_PHONE                     5
#define JETSAM_PRIORITY_UI_SUPPORT                8
#define JETSAM_PRIORITY_FOREGROUND_SUPPORT        9
#define JETSAM_PRIORITY_FOREGROUND               10
#define JETSAM_PRIORITY_AUDIO_AND_ACCESSORY      12
#define JETSAM_PRIORITY_CONDUCTOR                13
#define JETSAM_PRIORITY_HOME                     16
#define JETSAM_PRIORITY_EXECUTIVE                17
#define JETSAM_PRIORITY_IMPORTANT                18
#define JETSAM_PRIORITY_CRITICAL                 19

#define JETSAM_PRIORITY_MAX                      21

/* TODO - tune. This should probably be lower priority */
#define JETSAM_PRIORITY_DEFAULT                  18
#define JETSAM_PRIORITY_TELEPHONY                19

The higher the value, the higher the priority. The background application priority is 3, which is lower than the foreground application priority. The spring board (desktop program) is located in jetsam ﹣ priority ﹣ home16.

  • Why JetSam appears:
//kern_memorystatus.h
/*
 * Jetsam exit reason definitions - related to memorystatus
 *
 * When adding new exit reasons also update:
 *	JETSAM_REASON_MEMORYSTATUS_MAX
 *	kMemorystatusKilled... Cause enum
 *	memorystatus_kill_cause_name[]
 */
#define JETSAM_REASON_INVALID								0
#define JETSAM_REASON_GENERIC								1
#define JETSAM_REASON_MEMORY_HIGHWATER						2
#define JETSAM_REASON_VNODE									3
#define JETSAM_REASON_MEMORY_VMPAGESHORTAGE					4
#define JETSAM_REASON_MEMORY_PROCTHRASHING					5
#define JETSAM_REASON_MEMORY_FCTHRASHING					6
#define JETSAM_REASON_MEMORY_PERPROCESSLIMIT				7
#define JETSAM_REASON_MEMORY_DISK_SPACE_SHORTAGE			8
#define JETSAM_REASON_MEMORY_IDLE_EXIT						9
#define JETSAM_REASON_ZONE_MAP_EXHAUSTION					10
#define JETSAM_REASON_MEMORY_VMCOMPRESSOR_THRASHING			11
#define JETSAM_REASON_MEMORY_VMCOMPRESSOR_SPACE_SHORTAGE	12

#define JETSAM_REASON_MEMORYSTATUS_MAX	JETSAM_REASON_MEMORY_VMCOMPRESSOR_SPACE_SHORTAGE

/*
 * Jetsam exit reason definitions - not related to memorystatus
 */
#define JETSAM_REASON_CPULIMIT			100

/* Cause */
enum {
	kMemorystatusInvalid							= JETSAM_REASON_INVALID,
	kMemorystatusKilled								= JETSAM_REASON_GENERIC,
	kMemorystatusKilledHiwat						= JETSAM_REASON_MEMORY_HIGHWATER,
	kMemorystatusKilledVnodes						= JETSAM_REASON_VNODE,
	kMemorystatusKilledVMPageShortage				= JETSAM_REASON_MEMORY_VMPAGESHORTAGE,
	kMemorystatusKilledProcThrashing				= JETSAM_REASON_MEMORY_PROCTHRASHING,
	kMemorystatusKilledFCThrashing					= JETSAM_REASON_MEMORY_FCTHRASHING,
	kMemorystatusKilledPerProcessLimit				= JETSAM_REASON_MEMORY_PERPROCESSLIMIT,
	kMemorystatusKilledDiskSpaceShortage			= JETSAM_REASON_MEMORY_DISK_SPACE_SHORTAGE,
	kMemorystatusKilledIdleExit						= JETSAM_REASON_MEMORY_IDLE_EXIT,
	kMemorystatusKilledZoneMapExhaustion			= JETSAM_REASON_ZONE_MAP_EXHAUSTION,
	kMemorystatusKilledVMCompressorThrashing		= JETSAM_REASON_MEMORY_VMCOMPRESSOR_THRASHING,
	kMemorystatusKilledVMCompressorSpaceShortage	= JETSAM_REASON_MEMORY_VMCOMPRESSOR_SPACE_SHORTAGE,
};

//kern_memorystatus.m
/* For logging clarity */
static const char *memorystatus_kill_cause_name[] = {
	""								,		/* kMemorystatusInvalid							*/
	"jettisoned"					,		/* kMemorystatusKilled							*/
	"highwater"						,		/* kMemorystatusKilledHiwat						*/
	"vnode-limit"					,		/* kMemorystatusKilledVnodes					*/
	"vm-pageshortage"				,		/* kMemorystatusKilledVMPageShortage			*/
	"proc-thrashing"				,		/* kMemorystatusKilledProcThrashing				*/
	"fc-thrashing"					,		/* kMemorystatusKilledFCThrashing				*/
	"per-process-limit"				,		/* kMemorystatusKilledPerProcessLimit			*/
	"disk-space-shortage"			,		/* kMemorystatusKilledDiskSpaceShortage			*/
	"idle-exit"						,		/* kMemorystatusKilledIdleExit					*/
	"zone-map-exhaustion"			,		/* kMemorystatusKilledZoneMapExhaustion			*/
	"vm-compressor-thrashing"		,		/* kMemorystatusKilledVMCompressorThrashing		*/
	"vm-compressor-space-shortage"	,		/* kMemorystatusKilledVMCompressorSpaceShortage	*/
};

Memory status initialization

Next, let's take a look at the key part of the initialization code of JETSAM thread in the MEMORYSTATUS? Init() function.

__private_extern__ void
memorystatus_init(void)
{
    ... 
	/* Initialize the jetsam_threads state array */
	jetsam_threads = kalloc(sizeof(struct jetsam_thread_state) * max_jetsam_threads);

	/* Initialize all the jetsam threads */
	for (i = 0; i < max_jetsam_threads; i++) {

		result = kernel_thread_start_priority(memorystatus_thread, NULL, 95 /* MAXPRI_KERNEL */, &jetsam_threads[i].thread);
		if (result == KERN_SUCCESS) {
			jetsam_threads[i].inited = FALSE;
			jetsam_threads[i].index = i;
			thread_deallocate(jetsam_threads[i].thread);
		} else {
			panic("Could not create memorystatus_thread %d", i);
		}
	}
}

According to the startup parameters and device performance of the kernel, Max JetSam threads will be opened (1 device with poor performance and 3 others). The priority of these threads is the highest level that the kernel can allocate (95, MAXPRI_KERNEL). And the order is increased for each thread (Note: the - 2-19 in the previous article is the process priority interval, while the 95 here is the thread priority, and the XNU thread priority range is 0-127).

MEMORYSTATUS? Thread

There is a special thread in the system to manage the memory state. When there is a problem in the memory state or the memory pressure is too high, some apps will be killed to reclaim memory through certain strategies.

Continue to see the code of MEMORYSTATUS? Thread memory state management thread:

static void
memorystatus_thread(void *param __unused, wait_result_t wr __unused)
{
	boolean_t post_snapshot = FALSE;
	uint32_t errors = 0;
	uint32_t hwm_kill = 0;
	boolean_t sort_flag = TRUE;
	boolean_t corpse_list_purged = FALSE;
	int	jld_idle_kills = 0;
	struct jetsam_thread_state *jetsam_thread = jetsam_current_thread();

	if (jetsam_thread->inited == FALSE) {
		/* 
		 * It's the first time the thread has run, so just mark the thread as privileged and block.
		 * This avoids a spurious pass with unset variables, as set out in <rdar://problem/9609402>.
		 */

		char name[32];
		thread_wire(host_priv_self(), current_thread(), TRUE);
		snprintf(name, 32, "VM_memorystatus_%d", jetsam_thread->index + 1);

		if (jetsam_thread->index == 0) {
			if (vm_pageout_state.vm_restricted_to_single_processor == TRUE) {
				thread_vm_bind_group_add();
			}
		}
		thread_set_thread_name(current_thread(), name);
		jetsam_thread->inited = TRUE;
		memorystatus_thread_block(0, memorystatus_thread);
	}
	
	KERNEL_DEBUG_CONSTANT(BSDDBG_CODE(DBG_BSD_MEMSTAT, BSD_MEMSTAT_SCAN) | DBG_FUNC_START,
			      memorystatus_available_pages, memorystatus_jld_enabled, memorystatus_jld_eval_period_msecs, memorystatus_jld_eval_aggressive_count,0);

	/*
	 * Jetsam aware version.
	 *
	 * The VM pressure notification thread is working it's way through clients in parallel.
	 *
	 * So, while the pressure notification thread is targeting processes in order of 
	 * increasing jetsam priority, we can hopefully reduce / stop it's work by killing 
	 * any processes that have exceeded their highwater mark.
	 *
	 * If we run out of HWM processes and our available pages drops below the critical threshold, then,
	 * we target the least recently used process in order of increasing jetsam priority (exception: the FG band).
	 */
	while (memorystatus_action_needed()) {
		boolean_t killed;
		int32_t priority;
		uint32_t cause;
		uint64_t jetsam_reason_code = JETSAM_REASON_INVALID;
		os_reason_t jetsam_reason = OS_REASON_NULL;

		cause = kill_under_pressure_cause;
		switch (cause) {
			case kMemorystatusKilledFCThrashing:
				jetsam_reason_code = JETSAM_REASON_MEMORY_FCTHRASHING;
				break;
			case kMemorystatusKilledVMCompressorThrashing:
				jetsam_reason_code = JETSAM_REASON_MEMORY_VMCOMPRESSOR_THRASHING;
				break;
			case kMemorystatusKilledVMCompressorSpaceShortage:
				jetsam_reason_code = JETSAM_REASON_MEMORY_VMCOMPRESSOR_SPACE_SHORTAGE;
				break;
			case kMemorystatusKilledZoneMapExhaustion:
				jetsam_reason_code = JETSAM_REASON_ZONE_MAP_EXHAUSTION;
				break;
			case kMemorystatusKilledVMPageShortage:
				/* falls through */
			default:
				jetsam_reason_code = JETSAM_REASON_MEMORY_VMPAGESHORTAGE;
				cause = kMemorystatusKilledVMPageShortage;
				break;
		}

		/* Highwater */
		boolean_t is_critical = TRUE;
		if (memorystatus_act_on_hiwat_processes(&errors, &hwm_kill, &post_snapshot, &is_critical)) {
			if (is_critical == FALSE) {
				/*
				 * For now, don't kill any other processes.
				 */
				break;
			} else {
				goto done;
			}
		}

		jetsam_reason = os_reason_create(OS_REASON_JETSAM, jetsam_reason_code);
		if (jetsam_reason == OS_REASON_NULL) {
			printf("memorystatus_thread: failed to allocate jetsam reason\n");
		}

		if (memorystatus_act_aggressive(cause, jetsam_reason, &jld_idle_kills, &corpse_list_purged, &post_snapshot)) {
			goto done;
		}

		/*
		 * memorystatus_kill_top_process() drops a reference,
		 * so take another one so we can continue to use this exit reason
		 * even after it returns
		 */
		os_reason_ref(jetsam_reason);

		/* LRU */
		killed = memorystatus_kill_top_process(TRUE, sort_flag, cause, jetsam_reason, &priority, &errors);
		sort_flag = FALSE;

		if (killed) {
			if (memorystatus_post_snapshot(priority, cause) == TRUE) {

        			post_snapshot = TRUE;
			}

			/* Jetsam Loop Detection */
			if (memorystatus_jld_enabled == TRUE) {
				if ((priority == JETSAM_PRIORITY_IDLE) || (priority == system_procs_aging_band) || (priority == applications_aging_band)) {
					jld_idle_kills++;
				} else {
					/*
					 * We've reached into bands beyond idle deferred.
					 * We make no attempt to monitor them
					 */
				}
			}

			if ((priority >= JETSAM_PRIORITY_UI_SUPPORT) && (total_corpses_count() > 0) && (corpse_list_purged == FALSE)) {
				/*
				 * If we have jetsammed a process in or above JETSAM_PRIORITY_UI_SUPPORT
				 * then we attempt to relieve pressure by purging corpse memory.
				 */
				task_purge_all_corpses();
				corpse_list_purged = TRUE;
			}
			goto done;
		}
		
		if (memorystatus_avail_pages_below_critical()) {
			/*
			 * Still under pressure and unable to kill a process - purge corpse memory
			 */
			if (total_corpses_count() > 0) {
				task_purge_all_corpses();
				corpse_list_purged = TRUE;
			}

			if (memorystatus_avail_pages_below_critical()) {
				/*
				 * Still under pressure and unable to kill a process - panic
				 */
				panic("memorystatus_jetsam_thread: no victim! available pages:%llu\n", (uint64_t)memorystatus_available_pages);
			}
		}
			
done:		

		/*
		 * We do not want to over-kill when thrashing has been detected.
		 * To avoid that, we reset the flag here and notify the
		 * compressor.
		 */
		if (is_reason_thrashing(kill_under_pressure_cause)) {
			kill_under_pressure_cause = 0;
#if CONFIG_JETSAM
			vm_thrashing_jetsam_done();
#endif /* CONFIG_JETSAM */
		} else if (is_reason_zone_map_exhaustion(kill_under_pressure_cause)) {
			kill_under_pressure_cause = 0;
		}

		os_reason_free(jetsam_reason);
	}

	kill_under_pressure_cause = 0;
	
	if (errors) {
		memorystatus_clear_errors();
	}

	if (post_snapshot) {
		proc_list_lock();
		size_t snapshot_size = sizeof(memorystatus_jetsam_snapshot_t) +
			sizeof(memorystatus_jetsam_snapshot_entry_t) * (memorystatus_jetsam_snapshot_count);
		uint64_t timestamp_now = mach_absolute_time();
		memorystatus_jetsam_snapshot->notification_time = timestamp_now;
		memorystatus_jetsam_snapshot->js_gencount++;
		if (memorystatus_jetsam_snapshot_count > 0 && (memorystatus_jetsam_snapshot_last_timestamp == 0 ||
				timestamp_now > memorystatus_jetsam_snapshot_last_timestamp + memorystatus_jetsam_snapshot_timeout)) {
			proc_list_unlock();
			int ret = memorystatus_send_note(kMemorystatusSnapshotNote, &snapshot_size, sizeof(snapshot_size));
			if (!ret) {
				proc_list_lock();
				memorystatus_jetsam_snapshot_last_timestamp = timestamp_now;
				proc_list_unlock();
			}
		} else {
			proc_list_unlock();
		}
	}

	KERNEL_DEBUG_CONSTANT(BSDDBG_CODE(DBG_BSD_MEMSTAT, BSD_MEMSTAT_SCAN) | DBG_FUNC_END,
		memorystatus_available_pages, 0, 0, 0, 0);

	memorystatus_thread_block(0, memorystatus_thread);
}

There are many codes. Let's analyze them one by one.

Judgement condition

We can see that the core code is in the while (MEMORYSTATUS? Action? Needed()) loop, and MEMORYSTATUS? Action? Needed() is the core judgment condition for triggering OOM.

/* Does cause indicate vm or fc thrashing? */
static boolean_t 
is_reason_thrashing(unsigned cause)
{
	switch (cause) {
	case kMemorystatusKilledFCThrashing:
	case kMemorystatusKilledVMCompressorThrashing:
	case kMemorystatusKilledVMCompressorSpaceShortage:
		return TRUE;
	default:
		return FALSE;
	}
}

/* Is the zone map almost full? */
static boolean_t 
is_reason_zone_map_exhaustion(unsigned cause)
{
	if (cause == kMemorystatusKilledZoneMapExhaustion)
		return TRUE;
	return FALSE;
}

static boolean_t memorystatus_action_needed(void)
{
	return (is_reason_thrashing(kill_under_pressure_cause) ||
			is_reason_zone_map_exhaustion(kill_under_pressure_cause) ||
	       memorystatus_available_pages <= memorystatus_available_pages_pressure);
}

Here, we judge whether the current memory resource is tight by accepting the memory pressure sent by the VM ﹣ pageout daemons (actually a thread). The situation of memory shortage may be as follows: Thrashing of operating system (frequent page in and out of memory takes up too much CPU), virtual memory exhaustion (for example, someone copies 1TB of data from hard disk to ZFS (dynamic file system) pool), or memory available page is lower than the threshold value of memory available page.

high-water

After the judgment condition is passed, that is, the current memory is in short supply. First, go to the memory status? Act? On? Hiwat? Processes logic.

/* Highwater */
boolean_t is_critical = TRUE;
if (memorystatus_act_on_hiwat_processes(&errors, &hwm_kill, &post_snapshot, &is_critical)) {
	if (is_critical == FALSE) {
		/*
		 * For now, don't kill any other processes.
		 */
		break;
	} else {
		goto done;
	}
}

This is the key method to trigger high water type OOM.

static boolean_t
memorystatus_act_on_hiwat_processes(uint32_t *errors, uint32_t *hwm_kill, boolean_t *post_snapshot, __unused boolean_t *is_critical)
{
	boolean_t purged = FALSE;
	boolean_t killed = memorystatus_kill_hiwat_proc(errors, &purged);

	if (killed) {
		*hwm_kill = *hwm_kill + 1;
		*post_snapshot = TRUE;
		return TRUE;
	} else {
		if (purged == FALSE) {
			/* couldn't purge and couldn't kill */
			memorystatus_hwm_candidates = FALSE;
		}
	}

#if CONFIG_JETSAM
	/* No highwater processes to kill. Continue or stop for now? */
	if (!is_reason_thrashing(kill_under_pressure_cause) &&
		!is_reason_zone_map_exhaustion(kill_under_pressure_cause) &&
	    (memorystatus_available_pages > memorystatus_available_pages_critical)) {
		/*
		 * We are _not_ out of pressure but we are above the critical threshold and there's:
		 * - no compressor thrashing
		 * - enough zone memory
		 * - no more HWM processes left.
		 * For now, don't kill any other processes.
		 */
	
		if (*hwm_kill == 0) {
			memorystatus_thread_wasted_wakeup++;
		}

		*is_critical = FALSE;

		return TRUE;
	}
#endif /* CONFIG_JETSAM */

	return FALSE;
}

MEMORYSTATUS? Act? On? Hiwat? Processes directly calls MEMORYSTATUS? Kill? Hiwat? Proc.

static boolean_t
memorystatus_kill_hiwat_proc(uint32_t *errors, boolean_t *purged)
{
	pid_t aPid = 0;
	proc_t p = PROC_NULL, next_p = PROC_NULL;
	boolean_t new_snapshot = FALSE, killed = FALSE, freed_mem = FALSE;
	unsigned int i = 0;
	uint32_t aPid_ep;
	os_reason_t jetsam_reason = OS_REASON_NULL;
	KERNEL_DEBUG_CONSTANT(BSDDBG_CODE(DBG_BSD_MEMSTAT, BSD_MEMSTAT_JETSAM_HIWAT) | DBG_FUNC_START,
		memorystatus_available_pages, 0, 0, 0, 0);
	
	jetsam_reason = os_reason_create(OS_REASON_JETSAM, JETSAM_REASON_MEMORY_HIGHWATER);
	if (jetsam_reason == OS_REASON_NULL) {
		printf("memorystatus_kill_hiwat_proc: failed to allocate exit reason\n");
	}

	proc_list_lock();
	
	next_p = memorystatus_get_first_proc_locked(&i, TRUE);
	while (next_p) {
		uint64_t footprint_in_bytes = 0;
		uint64_t memlimit_in_bytes  = 0;
		boolean_t skip = 0;

		p = next_p;
		next_p = memorystatus_get_next_proc_locked(&i, p, TRUE);
		
		aPid = p->p_pid;
		aPid_ep = p->p_memstat_effectivepriority;
		
		if (p->p_memstat_state  & (P_MEMSTAT_ERROR | P_MEMSTAT_TERMINATED)) {
			continue;
		}
		
		/* skip if no limit set */
		if (p->p_memstat_memlimit <= 0) {
			continue;
		}

		footprint_in_bytes = get_task_phys_footprint(p->task);
		memlimit_in_bytes  = (((uint64_t)p->p_memstat_memlimit) * 1024ULL * 1024ULL);	/* convert MB to bytes */
		skip = (footprint_in_bytes <= memlimit_in_bytes);

#if CONFIG_JETSAM && (DEVELOPMENT || DEBUG)
		if (!skip && (memorystatus_jetsam_policy & kPolicyDiagnoseActive)) {
			if (p->p_memstat_state & P_MEMSTAT_DIAG_SUSPENDED) {
				continue;
			}
		}
#endif /* CONFIG_JETSAM && (DEVELOPMENT || DEBUG) */

#if CONFIG_FREEZE
		if (!skip) {
			if (p->p_memstat_state & P_MEMSTAT_LOCKED) {
				skip = TRUE;
			} else {
				skip = FALSE;
			}				
		}
#endif

		if (skip) {
			continue;
		} else {

			if (memorystatus_jetsam_snapshot_count == 0) {
				memorystatus_init_jetsam_snapshot_locked(NULL,0);
				new_snapshot = TRUE;
			}
	
			if (proc_ref_locked(p) == p) {
				/*
				 * Mark as terminated so that if exit1() indicates success, but the process (for example)
				 * is blocked in task_exception_notify(), it'll be skipped if encountered again - see
				 * <rdar://problem/13553476>. This is cheaper than examining P_LEXIT, which requires the
				 * acquisition of the proc lock.
				 */
				p->p_memstat_state |= P_MEMSTAT_TERMINATED;

				proc_list_unlock();
			} else {
				/*
				 * We need to restart the search again because
				 * proc_ref_locked _can_ drop the proc_list lock
				 * and we could have lost our stored next_p via
				 * an exit() on another core.
				 */
				i = 0;
				next_p = memorystatus_get_first_proc_locked(&i, TRUE);
				continue;
			}
		
			freed_mem = memorystatus_kill_proc(p, kMemorystatusKilledHiwat, jetsam_reason, &killed); /* purged and/or killed 'p' */

			/* Success? */
			if (freed_mem) {
				if (killed == FALSE) {
					/* purged 'p'..don't reset HWM candidate count */
					*purged = TRUE;

					proc_list_lock();
					p->p_memstat_state &= ~P_MEMSTAT_TERMINATED;
					proc_list_unlock();
				}
				proc_rele(p);
				goto exit;
			}
			/*
			 * Failure - first unwind the state,
			 * then fall through to restart the search.
			 */
			proc_list_lock();
			proc_rele_locked(p);
			p->p_memstat_state &= ~P_MEMSTAT_TERMINATED;
			p->p_memstat_state |= P_MEMSTAT_ERROR;
			*errors += 1;

			i = 0;
			next_p = memorystatus_get_first_proc_locked(&i, TRUE);
		}
	}
	
	proc_list_unlock();
	
exit:
	os_reason_free(jetsam_reason);

	/* Clear snapshot if freshly captured and no target was found */
	if (new_snapshot && !killed) {
		proc_list_lock();
		memorystatus_jetsam_snapshot->entry_count = memorystatus_jetsam_snapshot_count = 0;
		proc_list_unlock();
	}
	
	KERNEL_DEBUG_CONSTANT(BSDDBG_CODE(DBG_BSD_MEMSTAT, BSD_MEMSTAT_JETSAM_HIWAT) | DBG_FUNC_END, 
			      memorystatus_available_pages, killed ? aPid : 0, 0, 0, 0);

	return killed;
}

First, go to the priority queue to get the process with the lowest priority through MEMORYSTATUS ﹐ get ﹐ first ﹐ proc ﹐ locked (& I, true). If the process memory is less than the threshold value (Footprint in bytes < = memlimit in bytes), continue to find the next process with low priority level, MEMORYSTATUS get next proc locked, until the process with memory over the threshold value is found, the process will be killed through MEMORYSTATUS do kill, and the cycle will end.

normal kill

However, the threshold value of high water is high, which is not easy to trigger. If you can't finish any process through the high water related code, you will go to the MEMORYSTATUS? Act? Aggressive() function, which is where most of the OOM occurs.

static boolean_t
memorystatus_act_aggressive(uint32_t cause, os_reason_t jetsam_reason, int *jld_idle_kills, boolean_t *corpse_list_purged, boolean_t *post_snapshot)
{
	if (memorystatus_jld_enabled == TRUE) {

		boolean_t killed;
		uint32_t errors = 0;

		/* Jetsam Loop Detection - locals */
		memstat_bucket_t *bucket;
		int		jld_bucket_count = 0;
		struct timeval	jld_now_tstamp = {0,0};
		uint64_t 	jld_now_msecs = 0;
		int		elevated_bucket_count = 0;

		/* Jetsam Loop Detection - statics */
		static uint64_t  jld_timestamp_msecs = 0;
		static int	 jld_idle_kill_candidates = 0;	/* Number of available processes in band 0,1 at start */
		static int	 jld_eval_aggressive_count = 0;		/* Bumps the max priority in aggressive loop */
		static int32_t   jld_priority_band_max = JETSAM_PRIORITY_UI_SUPPORT;
		/*
		 * Jetsam Loop Detection: attempt to detect
		 * rapid daemon relaunches in the lower bands.
		 */
		
		microuptime(&jld_now_tstamp);

		/*
		 * Ignore usecs in this calculation.
		 * msecs granularity is close enough.
		 */
		jld_now_msecs = (jld_now_tstamp.tv_sec * 1000);

		proc_list_lock();
		switch (jetsam_aging_policy) {
		case kJetsamAgingPolicyLegacy:
			bucket = &memstat_bucket[JETSAM_PRIORITY_IDLE];
			jld_bucket_count = bucket->count;
			bucket = &memstat_bucket[JETSAM_PRIORITY_AGING_BAND1];
			jld_bucket_count += bucket->count;
			break;
		case kJetsamAgingPolicySysProcsReclaimedFirst:
		case kJetsamAgingPolicyAppsReclaimedFirst:
			bucket = &memstat_bucket[JETSAM_PRIORITY_IDLE];
			jld_bucket_count = bucket->count;
			bucket = &memstat_bucket[system_procs_aging_band];
			jld_bucket_count += bucket->count;
			bucket = &memstat_bucket[applications_aging_band];
			jld_bucket_count += bucket->count;
			break;
		case kJetsamAgingPolicyNone:
		default:
			bucket = &memstat_bucket[JETSAM_PRIORITY_IDLE];
			jld_bucket_count = bucket->count;
			break;
		}

		bucket = &memstat_bucket[JETSAM_PRIORITY_ELEVATED_INACTIVE];
		elevated_bucket_count = bucket->count;

		proc_list_unlock();

		/*
		 * memorystatus_jld_eval_period_msecs is a tunable
		 * memorystatus_jld_eval_aggressive_count is a tunable
		 * memorystatus_jld_eval_aggressive_priority_band_max is a tunable
		 */
		if ( (jld_bucket_count == 0) || 
		     (jld_now_msecs > (jld_timestamp_msecs + memorystatus_jld_eval_period_msecs))) {

			/* 
			 * Refresh evaluation parameters 
			 */
			jld_timestamp_msecs	 = jld_now_msecs;
			jld_idle_kill_candidates = jld_bucket_count;
			*jld_idle_kills		 = 0;
			jld_eval_aggressive_count = 0;
			jld_priority_band_max	= JETSAM_PRIORITY_UI_SUPPORT;
		}

		if (*jld_idle_kills > jld_idle_kill_candidates) {
			jld_eval_aggressive_count++;

#if DEVELOPMENT || DEBUG
			printf("memorystatus: aggressive%d: beginning of window: %lld ms, : timestamp now: %lld ms\n",
					jld_eval_aggressive_count,
					jld_timestamp_msecs,
					jld_now_msecs);
			printf("memorystatus: aggressive%d: idle candidates: %d, idle kills: %d\n",
					jld_eval_aggressive_count,
					jld_idle_kill_candidates,
					*jld_idle_kills);
#endif /* DEVELOPMENT || DEBUG */

			if ((jld_eval_aggressive_count == memorystatus_jld_eval_aggressive_count) &&
			    (total_corpses_count() > 0) && (*corpse_list_purged == FALSE)) {
				/*
				 * If we reach this aggressive cycle, corpses might be causing memory pressure.
				 * So, in an effort to avoid jetsams in the FG band, we will attempt to purge
				 * corpse memory prior to this final march through JETSAM_PRIORITY_UI_SUPPORT.
				 */
				task_purge_all_corpses();
				*corpse_list_purged = TRUE;
			}
			else if (jld_eval_aggressive_count > memorystatus_jld_eval_aggressive_count) {
				/* 
				 * Bump up the jetsam priority limit (eg: the bucket index)
				 * Enforce bucket index sanity.
				 */
				if ((memorystatus_jld_eval_aggressive_priority_band_max < 0) || 
				    (memorystatus_jld_eval_aggressive_priority_band_max >= MEMSTAT_BUCKET_COUNT)) {
					/*
					 * Do nothing.  Stick with the default level.
					 */
				} else {
					jld_priority_band_max = memorystatus_jld_eval_aggressive_priority_band_max;
				}
			}

			/* Visit elevated processes first */
			while (elevated_bucket_count) {

				elevated_bucket_count--;

				/*
				 * memorystatus_kill_elevated_process() drops a reference,
				 * so take another one so we can continue to use this exit reason
				 * even after it returns.
				 */

				os_reason_ref(jetsam_reason);
				killed = memorystatus_kill_elevated_process(
					cause,
					jetsam_reason,
					JETSAM_PRIORITY_ELEVATED_INACTIVE,
					jld_eval_aggressive_count,
					&errors);

				if (killed) {
					*post_snapshot = TRUE;
					if (memorystatus_avail_pages_below_pressure()) {
						/*
						 * Still under pressure.
						 * Find another pinned processes.
						 */
						continue;
					} else {
						return TRUE;
					}
				} else {
					/*
					 * No pinned processes left to kill.
					 * Abandon elevated band.
					 */
					break;
				}
			}

			/*
			 * memorystatus_kill_top_process_aggressive() allocates its own
			 * jetsam_reason so the kMemorystatusKilledProcThrashing cause
			 * is consistent throughout the aggressive march.
			 */
			killed = memorystatus_kill_top_process_aggressive(
				kMemorystatusKilledProcThrashing,
				jld_eval_aggressive_count, 
				jld_priority_band_max, 
				&errors);
				
			if (killed) {
				/* Always generate logs after aggressive kill */
				*post_snapshot = TRUE;
				*jld_idle_kills = 0;
				return TRUE;
			} 
		}

		return FALSE;
	}

	return FALSE;
}

First, there is a JLD bucket count, which contains the number of low priority processes that can be killed directly. According to jetsam ﹣ aging ﹣ policy, determine which priority types of processes need to be killed directly (normally, it is the processes with very low priority and some processes that can be recycled at any time under normal circumstances: jetsam ﹣ priority ﹣ idle, system ﹣ procs ﹣ aging ﹣ band, applications ﹣ aging ﹣ band).

If the memory pressure still exists, kill the background process through MEMORYSTATUS? Kill? Upgraded? Process. Every time a background process is killed, check the memory pressure through MEMORYSTATUS? Available? Pages. If the MEMORYSTATUS? Available? Pages is still less than the threshold value, the next process will continue to be killed.

If you kill all low priority processes and memory pressure, then you can kill the process with the lowest priority through MEMORYSTATUS? Kill? Top? Process? Aggressive. Here is the key to trigger FOOM. If the current foreground process is the lowest priority process, FOOM will occur.

LRU kills top process

If the above MEMORYSTATUS? Act? Aggressive function does not kill any processes, then you need to kill the first process in the Jetsam queue through LRU.

/*
 * memorystatus_kill_top_process() drops a reference,
 * so take another one so we can continue to use this exit reason
 * even after it returns
 */
os_reason_ref(jetsam_reason);

/* LRU */
killed = memorystatus_kill_top_process(TRUE, sort_flag, cause, jetsam_reason, &priority, &errors);
sort_flag = FALSE;

if (killed) {
	if (memorystatus_post_snapshot(priority, cause) == TRUE) {

			post_snapshot = TRUE;
	}

	/* Jetsam Loop Detection */
	if (memorystatus_jld_enabled == TRUE) {
		if ((priority == JETSAM_PRIORITY_IDLE) || (priority == system_procs_aging_band) || (priority == applications_aging_band)) {
			jld_idle_kills++;
		} else {
			/*
			 * We've reached into bands beyond idle deferred.
			 * We make no attempt to monitor them
			 */
		}
	}

	if ((priority >= JETSAM_PRIORITY_UI_SUPPORT) && (total_corpses_count() > 0) && (corpse_list_purged == FALSE)) {
		/*
		 * If we have jetsammed a process in or above JETSAM_PRIORITY_UI_SUPPORT
		 * then we attempt to relieve pressure by purging corpse memory.
		 */
		task_purge_all_corpses();
		corpse_list_purged = TRUE;
	}
	goto done;
}

if (memorystatus_avail_pages_below_critical()) {
	/*
	 * Still under pressure and unable to kill a process - purge corpse memory
	 */
	if (total_corpses_count() > 0) {
		task_purge_all_corpses();
		corpse_list_purged = TRUE;
	}

	if (memorystatus_avail_pages_below_critical()) {
		/*
		 * Still under pressure and unable to kill a process - panic
		 */
		panic("memorystatus_jetsam_thread: no victim! available pages:%llu\n", (uint64_t)memorystatus_available_pages);
	}
}

When all processes are completed, some finishing work will be done.

		/*
		 * We do not want to over-kill when thrashing has been detected.
		 * To avoid that, we reset the flag here and notify the
		 * compressor.
		 */
		if (is_reason_thrashing(kill_under_pressure_cause)) {
			kill_under_pressure_cause = 0;
#if CONFIG_JETSAM
			vm_thrashing_jetsam_done();
#endif /* CONFIG_JETSAM */
		} else if (is_reason_zone_map_exhaustion(kill_under_pressure_cause)) {
			kill_under_pressure_cause = 0;
		}

		os_reason_free(jetsam_reason);
	}

	kill_under_pressure_cause = 0;
	
	if (errors) {
		memorystatus_clear_errors();
	}

	if (post_snapshot) {
		proc_list_lock();
		size_t snapshot_size = sizeof(memorystatus_jetsam_snapshot_t) +
			sizeof(memorystatus_jetsam_snapshot_entry_t) * (memorystatus_jetsam_snapshot_count);
		uint64_t timestamp_now = mach_absolute_time();
		memorystatus_jetsam_snapshot->notification_time = timestamp_now;
		memorystatus_jetsam_snapshot->js_gencount++;
		if (memorystatus_jetsam_snapshot_count > 0 && (memorystatus_jetsam_snapshot_last_timestamp == 0 ||
				timestamp_now > memorystatus_jetsam_snapshot_last_timestamp + memorystatus_jetsam_snapshot_timeout)) {
			proc_list_unlock();
			int ret = memorystatus_send_note(kMemorystatusSnapshotNote, &snapshot_size, sizeof(snapshot_size));
			if (!ret) {
				proc_list_lock();
				memorystatus_jetsam_snapshot_last_timestamp = timestamp_now;
				proc_list_unlock();
			}
		} else {
			proc_list_unlock();
		}
	}

	KERNEL_DEBUG_CONSTANT(BSDDBG_CODE(DBG_BSD_MEMSTAT, BSD_MEMSTAT_SCAN) | DBG_FUNC_END,
		memorystatus_available_pages, 0, 0, 0, 0);

	memorystatus_thread_block(0, memorystatus_thread);

When bumps are detected, the system does not want to overkill. To avoid this, the system resets the flag here and notifies compressor. If you need to record the current memory snapshot, suspend the current thread after recording, and wake up again when encountering OOM after waiting.

How to trigger OOM detection

Once the MEMORYSTATUS? Thread is initialized, an OOM is detected immediately.

In task.c, when the physical memory reaches the limit, the callback will be triggered, and the MEMORYSTATUS? On? Ledger? Footprint? Exceeded will be called to synchronously trigger the OOM of per process limit type.

Similar to the previous one, for example, MEMORYSTATUS? Kill? On? Vnode? Limit is also triggered synchronously. In other words, the MEMORYSTATUS? Kill? Process? Sync method is finally called to directly kill the corresponding process. If the pid is - 1, the process in the queue head will be killed.

static boolean_t 
memorystatus_kill_process_sync(pid_t victim_pid, uint32_t cause, os_reason_t jetsam_reason) {
	boolean_t res;

	uint32_t errors = 0;

	if (victim_pid == -1) {
		/* No pid, so kill first process */
		res = memorystatus_kill_top_process(TRUE, TRUE, cause, jetsam_reason, NULL, &errors);
	} else {
		res = memorystatus_kill_specific_process(victim_pid, cause, jetsam_reason);
	}
	
	if (errors) {
		memorystatus_clear_errors();
	}

	if (res == TRUE) {
		/* Fire off snapshot notification */
		proc_list_lock();
		size_t snapshot_size = sizeof(memorystatus_jetsam_snapshot_t) + 
			sizeof(memorystatus_jetsam_snapshot_entry_t) * memorystatus_jetsam_snapshot_count;
		uint64_t timestamp_now = mach_absolute_time();
		memorystatus_jetsam_snapshot->notification_time = timestamp_now;
		if (memorystatus_jetsam_snapshot_count > 0 && (memorystatus_jetsam_snapshot_last_timestamp == 0 ||
				timestamp_now > memorystatus_jetsam_snapshot_last_timestamp + memorystatus_jetsam_snapshot_timeout)) {
			proc_list_unlock();
			int ret = memorystatus_send_note(kMemorystatusSnapshotNote, &snapshot_size, sizeof(snapshot_size));
			if (!ret) {
				proc_list_lock();
				memorystatus_jetsam_snapshot_last_timestamp = timestamp_now;
				proc_list_unlock();
			}
		} else {
			proc_list_unlock();
		}
	}

	return res;
}

And MEMORYSTATUS? Kill? On? VM? Compressor? Space? Shortcut, MEMORYSTATUS? Kill? On? VM? Compressor? Thrashing, MEMORYSTATUS? Kill? On? FC? Thrashing are all triggered asynchronously, that is to say, they call the MEMORYSTATUS? Kill? Process? Sync method.

static boolean_t 
memorystatus_kill_process_async(pid_t victim_pid, uint32_t cause) {
	/*
	 * TODO: allow a general async path
	 *
	 * NOTE: If a new async kill cause is added, make sure to update memorystatus_thread() to
	 * add the appropriate exit reason code mapping.
	 */
	if ((victim_pid != -1) ||
			(cause != kMemorystatusKilledVMPageShortage &&
			cause != kMemorystatusKilledVMCompressorThrashing &&
			cause != kMemorystatusKilledVMCompressorSpaceShortage &&
			cause != kMemorystatusKilledFCThrashing &&
			cause != kMemorystatusKilledZoneMapExhaustion)) {
		return FALSE;
	}
    
	kill_under_pressure_cause = cause;
	memorystatus_thread_wake();
	return TRUE;
}

That is to say, it finally wakes up the memory status "thread" to execute the set of processes that we just checked the source code.

If the pid is - 1, the asynchronous method is called; otherwise, the synchronous method is called.

Sorting out the source logic process

  1. JetSam thread initializes and receives memory pressure from outside
  2. If the received memory pressure is that the current physical memory reaches the limit, the per process limit type OOM will be triggered synchronously to exit the process
  3. If the received memory pressure is of other types, wake up the JetSam thread and judge that the kill "under" pressure "value is kmemorystuskilledvmthrashing, kmemorystuskilledfcthrashing, kmemorystuskilledzonemapexation, or when the currently available memory memory" available "pages is less than the threshold value, enter the OOM logic.
  4. Traverse each process with the lowest priority, and judge whether the current process is higher than the threshold value according to the phys ﹣ footprint. If it is not higher than the threshold value, search for the next process with the lowest priority according to the needs, until it is found, trigger the high water type OOM
  5. At this time, go back to a process with lower collection priority or a process that can be recycled at any time under normal circumstances, and go to the judgment logic of 4 again
  6. After all low priority processes or processes recovered at any time in class under normal circumstances are killed, if the MEMORYSTATUS ﹣ available ﹣ pages is still less than the threshold value, kill the background processes first. For each process killed, judge whether the MEMORYSTATUS ﹣ available ﹣ pages is still less than the threshold value. If it is already less than the threshold value, suspend the thread and wait for wakeup
  7. When all background processes are killed, call MEMORYSTATUS? Kill? Top? Process? Aggressive to kill the foreground process, suspend the thread and wait for the wake-up
  8. If the above MEMORYSTATUS? Kill? Top? Process? Aggressive does not kill any process, kill the first process in the Jetsam queue through LRU, suspend the thread and wait for wakeup

How to determine the occurrence of OOM

facebook and wechat's Matrix are both exclusion methods. During Matrix initialization, the checkRebootType method is called to determine whether an OOM has occurred. The specific process is as follows:

  1. If the current device is DEBUG, return directly and do not continue.
  2. Whether there was a normal crash when the app was opened last time. If not, continue to execute
  3. After the app was opened last time, it is whether the user actively exits the app (listening for UIApplicationWillTerminateNotification message). If not, continue to execute
  4. Whether to call exit related functions (monitored by atexit function) after the app was last opened. If not, continue to execute
  5. Whether to suspend or execute backgroundFetch after opening the app last time. If the app is not killed by the watchdog at this time, it is a kind of OOM. The Matrix is named Suspend OOM. If not, continue to execute
  6. Whether the uuid of the app has changed, if not continue to execute
  7. Whether the system has been upgraded since the app was last opened. If not, continue to execute
  8. After the app was last opened, did the device restart? If not, continue to execute
  9. When the app was last opened, whether the app was in the background? If so, the Background OOM was triggered. If not, the execution will continue
  10. After opening the app last time, whether the app is in the foreground and whether the main thread is stuck. If it is not, the foregroup oom is triggered.

In fact, most of the OOM s we talk about are FOOM. Because if our program is in the background, the priority is very low. Even if we don't occupy a lot of memory, we may kill our program in the background because the foreground application occupies a lot of memory. This is a systematic mechanism. We don't have many ways.

So focus on FOOM. For FOOM, we need to focus on dirty pages and IOKit mappings. Of course, we need to pay attention to the caching done by the system, such as pictures, fonts, etc. For the problem monitoring and solution of OOM, please refer to Matrix and OOMDetector Two open source libraries. At present, the monitoring of OOM is also in the exploration stage. In the future, if you have some experience in monitoring and processing OOM, you will actively share it with us.

Reference material

OOM exploration: XNU memory state management

Do you really know OOM? ——JD iOS APP memory optimization record

Handling low memory conditions in iOS and Mavericks

IOS out of memory principle and Scheme Research

iOS wechat memory monitoring

71 original articles published, 34 praised, 90000 visitors+
Private letter follow

Topics: iOS less Mobile xcode