JetsamEvent 是 iOS 唯一由系统官方生成、且专门记录"因内存原因终止进程"的日志。它不记录线程堆栈,却完整刻画了发生强杀那一刻整机的内存分布——这使它成为 OOM 归因、阈值建模、后台生命周期治理最权威的数据源。

本文聚焦 JetsamEvent 日志本身:存放位置、文件结构、字段含义、reason 类型、案例解读、自动化批量解析脚本,以及机型内存上限/iOS 版本差异等实战要点。OOM 监控治理的整体方法论见 崩溃-治理


1. Jetsam 与 JetsamEvent 是什么

iOS 通过 XNU 内核的 memorystatus 子系统(俗称 Jetsam)做整机内存管理:

  • 前台/后台进程按 Jetsam 优先级排序;
  • 当系统整体可用物理页(vm_page_free_count)或单个进程 phys_footprint 触发阈值时,按优先级杀掉进程;
  • 被杀时,内核在 /var/mobile/Library/Logs/CrashReporter/ 生成 JetsamEvent-yyyy-MM-dd-HHmmss.ips 文件,并通过 DiagnosticsReporting 暴露给 设置 → 隐私与安全性 → 分析与改进 → 分析数据

需要理解两个关键结论:

  1. JetsamEvent 不是单个进程的崩溃日志,而是整机一次 Jetsam 事件的快照。 一个文件里往往包含几十个进程条目,只有其中一个(或少数)被真正杀掉;其余进程是作为"当时整机内存画像"被附带记录。
  2. JetsamEvent 不含线程堆栈。 它的价值在于告诉你"谁占用了多少内存"和"为什么被杀",而不是"在哪一行代码被杀"。堆栈需要由 App 自己的 Memory Dump / MetricKit 补齐。

2. 获取方式

场景路径
真机设置页查看设置 → 隐私与安全性 → 分析与改进 → 分析数据,文件名 JetsamEvent-*.ips
Xcode 连接真机Window → Devices and Simulators → 选中设备 → View Device Logs
sysdiagnose 包解压后位于 crashes_and_spins/*.ips
越狱设备/var/mobile/Library/Logs/CrashReporter/JetsamEvent-*.ips
自动上报App 下次启动时扫描沙盒外的 Apple 日志目录不可行,只能依赖 MetricKitMXCrashDiagnostic/MXAppExitMetric(iOS 14+)

⚠️ iOS 沙盒下 App 无法直接读取 /var/mobile/Library/Logs/ 的原始 ips 文件,线上规模化采集必须走 MetricKit。JetsamEvent 原始日志主要用于研发期抓真机、或用户反馈场景的手工分析。


3. 文件整体结构

iOS 14 起 .ips 统一为"双段 JSON":

{ header JSON,一行 }
{ body JSON,多行,JSON5 风格 }

解析时必须按换行把两段拆开后分别 JSON.parse,不能整体当作一个 JSON 解析。

3.1 Header 段关键字段

{
  "app_name": "kernel_task",
  "timestamp": "2025-03-11 22:14:05.00 +0800",
  "app_version": "",
  "bug_type": "298",
  "os_version": "iPhone OS 17.4 (21E219)",
  "incident_id": "8D2E...",
  "name": "kernel_task"
}
字段含义
bug_type298 即 JetsamEvent;109 是普通崩溃;288 是 CPU Resource Limit
os_version内含 build 号,用来定位 Jetsam 行为差异(iOS 14 vs 17)
timestamp事件发生时刻,时区为设备本地时区
incident_id全系统唯一 ID,用于与 MetricKit 结果交叉对齐

判定 JetsamEvent 的唯一标准是 bug_type == 298,而不是文件名。

3.2 Body 段骨架

{
  "crashReporterKey": "...",
  "kernel": "Darwin Kernel Version 23.4.0 ...",
  "product": "iPhone14,5",
  "incident": "...",
  "date": "2025-03-11 22:14:05.00 +0800",
  "build": "21E219",
  "timestamp": 764388845123,
  "genCount": 0,
  "reason": "per-process-limit",
  "pageSize": 16384,
  "largestProcess": "YourApp",
  "memoryStatus": { ... },
  "processes": [ { ... }, { ... } ]
}

4. Body 顶层字段详解

4.1 pageSize

最关键的一个数。JetsamEvent 里所有内存字段都以 page(而不是字节)为单位,真实字节数必须乘以 pageSize

真实占用(字节) = rpages × pageSize
设备架构典型 pageSize
A7~A13(iPhone 5s ~ iPhone 11)16384(16 KB)
A14+(iPhone 12 及以后)16384(16 KB)
Apple Silicon iPad16384
模拟器(x86_64)4096

真机 iOS 侧 pageSize 自 A7 起就是 16 KB,但部分模拟器/老设备导出日志仍可能是 4 KB,脚本不要写死。

4.2 reason

Jetsam 触发的直接原因,是后续归因的主开关。常见取值:

reason含义关注点
per-process-limit单进程 phys_footprint 超过专属限额本进程就是凶手,看 processes[].reason
vm-pageshortage系统整体物理页不足,按优先级清理与整机负载/其他进程有关
fc-thrashing文件缓存颠簸(大文件非顺序读/写)I/O 行为,典型于视频、数据库
vnode-limit进程打开 vnode 过多(iOS 15+ ≈ 10000)fd 泄漏、mmap/dlopen 无上限
zone-map-exhaustion内核 zone 用尽(内核对象泄漏)IOKit/驱动/系统服务,App 侧罕见
disk-space-shortage磁盘空间不足缓存/日志膨胀
highwater常驻进程跨过 high-water mark多用于系统守护进程
idle-exit后台空闲退出(正常生命周期)非异常,和 OOM 无关

4.3 largestProcess

Jetsam 决策时占用最大的进程名。它不等于"被杀的进程"——被杀的是 processes[i].reason 非空的那一项。两者一致多半是"凶手就是最大头",不一致则往往意味着前台进程优先级低导致被先清理。

4.4 genCount

同一个系统启动周期内 Jetsam 的生成计数。连续出现很大的 genCount 说明整机长期处于内存压力区间,不是偶发。


5. memoryStatus 整机内存画像

"memoryStatus": {
  "compressorSize": 98256,
  "compressions": 58422199,
  "decompressions": 41203311,
  "zoneMapCap": 2684354560,
  "largestZone": "APFS_4K_OBJS",
  "largestZoneSize": 168820736,
  "pageSize": 16384,
  "uncompressed": 110842,
  "busyBufferCount": 128,
  "memoryPages": {
    "active": 188432,
    "inactive": 120221,
    "free": 3142,
    "speculative": 8221,
    "throttled": 0,
    "wired": 94210,
    "purgeable": 9822,
    "fileBacked": 76540,
    "anonymous": 232113,
    "compressor": 98256
  }
}

读图口诀:

  • free 极小 + compressor 很大 → 整机物理内存被榨干,压缩池已经撑起半壁江山,典型的 vm-pageshortage 环境。
  • compressions / decompressions 持续飙升 → 内存颠簸(swap 不停压缩解压),性能与耗电都会受损。
  • wired 异常大 → 内核占用偏高,可能是驱动/扩展问题,并非普通 App 可优化。
  • anonymous(匿名内存) ≫ fileBacked → 大量 heap/stack/脏页,App 堆内存增长是主因。

换算成 MBpages × pageSize / 1024 / 1024


6. processes 数组——核心证物

数组中每个元素是进程在 Jetsam 时刻的快照,按优先级/内存排序,被杀进程会带 reason 字段。

6.1 常见字段

{
  "pid": 1234,
  "uuid": "C3E6B3A1-...",
  "name": "YourApp",
  "states": ["frontmost"],
  "priority": 10,
  "rpages": 92160,
  "lifetimeMax": 98231,
  "fds": 128,
  "cpuTime": 42910,
  "idleDelta": 0,
  "genCount": 0,
  "coalition": 27,
  "killDelta": 0,
  "reason": "per-process-limit"
}
字段含义实战用法
pid / uuid进程 ID / 二进制 UUIDUUID 用来和 dSYM、MetricKit 对齐
name进程名,不含 bundle id需要 bundle id 时对照 processes[].uuid ↔ 自家符号表
states状态数组:frontmost / resume / suspended / daemon判断 FOOM vs BOOM 的依据
priorityJetsam 优先级,数字越大越容易被杀前台 ≈ 10,后台 ≈ 3;系统守护 < 0
rpages当前 resident 页数(含 compressed)rpages × pageSizephys_footprint
lifetimeMax生命周期内的峰值页数用于推断是否经历过内存尖刺
fds打开的文件描述符数排查 fd 泄漏、vnode-limit
cpuTime累积 CPU 时间(单位:ticks)结合 idleDelta 识别忙碌进程
coalitioncoalition 组 ID尤其重要:主 App、WebContent、Extension 通常同组
reason若非空,说明这个进程是被杀对象OOM 归因的主字段
killDelta相邻 Jetsam 事件的间隔(µs)判断是否"雪崩式"连续强杀

6.2 OOM 阈值反推

reason = per-process-limit 时:

OOM 阈值(MB) = rpages × pageSize / 1024 / 1024

pageSize = 16384rpages = 92160 为例 → 92160 × 16384 / 1024 / 1024 = 1440 MB

这就是该设备 + 该 iOS 版本 + 该前/后台状态下的 OOM 专属阈值。批量解析 100+ 条 JetsamEvent 就能建出一份机型 × 版本 × 前后台的阈值矩阵,远比靠"机型常量表"准确。

6.3 Coalition 视角

iOS 对 App 主进程 + WebContent 子进程 + 扩展 采用 coalition 共享内存账本:任一成员超标都会影响整组存活。解读时把同一 coalition 的条目拎出来对比:

coalition=27  YourApp          rpages=84000   frontmost
coalition=27  com.apple.WebKit rpages=63000   frontmost   <-- 凶手
coalition=27  YourAppExtension rpages=2100    background

上例看起来像 App 吃了 1.3 GB,实则 WebKit 子进程吃掉 1 GB,App 本体仅 1.3 GB 的差额。如果你的归因只统计主进程,就会错杀结论

6.4 状态与 FOOM / BOOM 判定

被杀进程 states典型结论
frontmostFOOM(前台 OOM),用户强感知
background / suspendedBOOM(后台 OOM),用户下次启动回到首页
daemon系统服务而非 App,与用户无关
frozen / suspended + reason=vm-pageshortage系统内存紧张导致的后台清理,非本 App 问题

7. reason 深度解读

7.1 per-process-limit

  • 语义:单进程 dirty memory + compressed ≥ 系统给该进程的 memorystatus_task_limit
  • 排查方向:本进程内部。按时间序列看最近几分钟:前台峰值、是否有大图、是否有数据泄漏、是否开了 com.apple.developer.kernel.increased-memory-limit 还超标。
  • 配套证据:最好同步拉本地 Memory Dump(存活对象 + 分配堆栈),OOM 归因从此从"猜"变成"查堆栈"。

7.2 vm-pageshortage

  • 语义:整机物理页不足,触发了"按 Jetsam 优先级清理"的动作,被杀进程不一定是用得最多的。
  • 排查方向:整机视角。重点看 memoryStatus.memoryPages.free / compressor、同 coalition 的兄弟进程、后台是否残留大块内存。
  • 治理方法:压低后台驻留(图片缓存、视频解码上下文、大数据结构),收到 UIApplicationDidReceiveMemoryWarningNotification务必真的释放

7.3 fc-thrashing

  • 语义:File Cache(UBC)颠簸——大量页在 fileBacked 与 compressor / swap 间反复腾挪。
  • 典型场景
    • 视频录制/播放在主线程大块 read;
    • SQLite/Core Data WAL 不断 mmap 大文件;
    • 把大图文件 mmap 后又到处随机访问。
  • 治理:用 dispatch_io 做顺序流式 I/O,mmap 记得 madvise(MADV_SEQUENTIAL) / MADV_DONTNEED 主动提示访问模式。

7.4 vnode-limit

  • 语义:一个进程同时打开的 vnode(文件系统对象)超过系统上限(iOS 15+ 约 10000)。
  • 触发:fd 泄漏、大量 mmap 不 munmap、Database 句柄未关、dlopen 无上限、Bundle 频繁枚举文件。
  • 排查:Instruments 的 File Activity;自研 hook open/close/mmap/munmap/dlopen 记录生命周期。

7.5 zone-map-exhaustion

  • 语义:内核对象 zone 用尽,通常与 App 无关,由系统服务或驱动泄漏触发;会伴随 largestZone 字段定位是哪个 zone。
  • 对 App:自己做不了什么,只能复现 + 给 Apple 提 Feedback。

7.6 disk-space-shortage

  • 语义:磁盘空间不足,影响整机稳定性。
  • 治理:Cache 目录分级清理、日志文件轮转上限、预留阈值时主动清理并弹提示。

7.7 idle-exit

  • 语义:后台空闲退出,属于正常生命周期,不是 OOM,不要算进崩溃率。

7.8 highwater

  • 语义:跨越 high-water mark。App Extension(分享扩展、Keyboard、Widget)阈值极低(50–120 MB),极易触发。
  • 治理:扩展侧用流式处理,严控图片解码尺寸;Keyboard 类扩展避免把模型/词表全量载入。

8. 实战案例解读

8.1 片段

{
  "reason": "per-process-limit",
  "pageSize": 16384,
  "largestProcess": "YourApp",
  "processes": [
    {
      "pid": 871,
      "name": "YourApp",
      "uuid": "C3E6B3A1-0F2A-4FB2-9C88-AAAA11112222",
      "states": ["frontmost"],
      "priority": 10,
      "rpages": 92160,
      "lifetimeMax": 93000,
      "fds": 312,
      "cpuTime": 86400,
      "coalition": 27,
      "reason": "per-process-limit"
    },
    {
      "pid": 874,
      "name": "com.apple.WebKit.WebContent",
      "states": ["frontmost"],
      "priority": 10,
      "rpages": 8400,
      "coalition": 27
    }
  ],
  "memoryStatus": {
    "memoryPages": { "free": 1200, "active": 175000, "inactive": 120000, "compressor": 90000 }
  }
}

8.2 解读步骤

  1. 类型reason = per-process-limit → 确认是单进程超限,不是系统整体内存压力。
  2. 被杀进程processes[0].reason 非空 → YourApp 本体被杀。
  3. 阈值92160 × 16384 = 1.5 GB。对比该机型在官方资料/其他日志中的常见阈值,若普遍 ~2 GB,说明当时系统可能已有其他压力动态下调了 limit(看 memoryPages.free = 1200 极低,的确吃紧)。
  4. 状态states = ["frontmost"] → FOOM,用户强感知。
  5. Coalition:同 coalition 内 WebContent 仅 132 MB,和 App 本体 1.5 GB 相比不是主因 → 真正的锅在主进程
  6. fds = 312:并非 vnode 超限级别,可以排除 fd 泄漏。
  7. 建议动作:打开本地 Memory Dump 找峰值对象 → 往往是全尺寸图片解码无界缓存;同时建议申请 com.apple.developer.kernel.increased-memory-limit 做兜底。

9. 自动化批量解析

9.1 快速拆分(bash + jq)

#!/bin/bash
# 将 .ips 拆成 header.json / body.json
file="$1"
awk 'NR==1{print > "header.json"; next} {print > "body.json"}' "$file"

jq '.bug_type, .os_version, .timestamp' header.json
jq '{reason, pageSize, largestProcess,
     pages: .memoryStatus.memoryPages,
     killed: [.processes[] | select(.reason != null) | {name, uuid, rpages, states, reason}]}' body.json

9.2 机型阈值矩阵(Python)

import json, os, re, csv
from collections import defaultdict

def parse_ips(path):
    with open(path, 'r', encoding='utf-8') as f:
        lines = f.read().split('\n', 1)
    header = json.loads(lines[0])
    body = json.loads(lines[1])
    if header.get('bug_type') != '298':
        return None
    killed = [p for p in body.get('processes', []) if p.get('reason')]
    if not killed:
        return None
    page = body.get('pageSize', 16384)
    return {
        'os': header.get('os_version', ''),
        'product': body.get('product', ''),
        'reason': body.get('reason'),
        'killed': [
            {
                'name': k['name'],
                'frontmost': 'frontmost' in k.get('states', []),
                'rpages': k['rpages'],
                'limit_mb': round(k['rpages'] * page / 1024 / 1024, 1),
            }
            for k in killed
        ],
    }

matrix = defaultdict(list)
for root, _, files in os.walk('./jetsam_logs'):
    for f in files:
        if f.startswith('JetsamEvent-') and f.endswith('.ips'):
            r = parse_ips(os.path.join(root, f))
            if not r:
                continue
            for k in r['killed']:
                key = (r['product'], re.search(r'iPhone OS \d+\.\d+', r['os']).group(), k['frontmost'])
                matrix[key].append(k['limit_mb'])

with open('oom_limits.csv', 'w', newline='') as f:
    w = csv.writer(f)
    w.writerow(['product', 'os', 'frontmost', 'samples', 'p50_mb', 'p90_mb', 'max_mb'])
    for (product, os_ver, front), arr in matrix.items():
        arr.sort()
        n = len(arr)
        p50 = arr[n // 2]
        p90 = arr[int(n * 0.9)]
        w.writerow([product, os_ver, front, n, p50, p90, max(arr)])

批量跑一段时间就能得到自己的机型 × iOS × 前后台阈值表,线上兜底时比网络上零散的"机型限额"更可信。


10. 常见机型阈值经验值

以下是社区实测 + 本文脚本汇总的一份常见经验值,仅用于兜底决策(真实阈值以 os_proc_available_memory() 实时值为准)。

机型总内存前台 OOM(常驻)前台 OOM(iOS 15+ entitlement)
iPhone 8 / X2–3 GB~1.3 GB~1.8 GB
iPhone 11 / XS / XR4 GB~1.8 GB~3.0 GB
iPhone 12 / 13 / 144–6 GB~2.8 GB~3.5–4.0 GB
iPhone 15 / 15 Pro6–8 GB~3.2 GB~5.0 GB
iPad Pro M1/M28–16 GB~4.5 GB~12 GB
Extension 通用50–120 MB

iOS 15 起,主 App 可以申请 com.apple.developer.kernel.increased-memory-limit(需 Apple 授权的机型、且用户未开启低电量模式等),拉高前台上限;扩展不支持。


11. iOS 版本差异

版本主要变化
iOS 13os_proc_available_memory() 新增;JetsamEvent 仍为"类 plist"格式
iOS 14日志统一为双段 JSON(header + body),字段更标准化;引入 MetricKitMXCrashDiagnosticexceptionType=EXC_RESOURCE, subtype=MEMORY)可线上回收 OOM
iOS 15支持 increased-memory-limit / extended-virtual-addressing entitlement;vnode-limit 阈值提高到 10000
iOS 16增加 MXAppExitMetric,能区分 foregroundOOM / backgroundOOM 等退出原因
iOS 17memoryPages 新增 anonymous / fileBacked 细分,更利于区分"堆内存" vs “缓存文件”
iOS 18压缩器策略进一步激进,compressor 占比常态化 > 20%

12. 与 MetricKit 的字段映射

线上批量归因的推荐路径是 MetricKit + 本地 Memory Dump,JetsamEvent 作为研发/客诉场景的补充证据。两者字段映射如下:

JetsamEvent 字段MetricKit 对应
bug_type = 298MXCrashDiagnostic.exceptionType = EXC_RESOURCE + subtype = MEMORY
reason = per-process-limitMXAppExitMetric.cumulativeMemoryResourceLimitExitCount(iOS 16+)
reason = vm-pageshortageMXAppExitMetric.cumulativeMemoryPressureExitCount(iOS 16+)
states: frontmostMXAppExitMetric.foregroundExitData
states: backgroundMXAppExitMetric.backgroundExitData
rpages × pageSizeMXCrashDiagnostic.virtualMemoryRegionInfo(只在部分崩溃里有)

实战建议:线上采集 MXAppExitMetric 拿 OOM 计数 + 前后台分布;研发阶段用 JetsamEvent 拉真机拆阈值;客诉阶段让用户导出 JetsamEvent + App 自己的 Memory Dump 交叉分析。