Process importance and the OOM killer
Android kills processes on purpose. The oom_adj score, the importance tiers, lmkd and PSI, and why clearing your recents saves less battery than it looks.
No phone has enough RAM to keep every app the user has opened. Android's answer is not to ask apps to quit — apps are bad at knowing when to quit — but for the system to kill them, in a defined order, and to restart them exactly where they were when the user comes back.
TL;DR#
- Processes are ranked by importance, expressed as an oom_adj score computed by ActivityManager.
- The tiers run from foreground (most important) through visible, perceptible, service and cached (least).
- lmkd is the userspace low-memory killer. It watches memory pressure, often via PSI, and kills from the bottom of the ranking.
- A killed app is restarted with its saved state when the user returns; the user is not supposed to notice.
- Clearing recents does not save meaningful battery. Cached processes are already doing almost nothing.
Mental model#
Think of a hospital with a fixed number of beds.
Patients are triaged: critical, serious, stable, discharged-but-still-here. When a new patient needs a bed and none is free, the least critical patient is moved out — not because they are unimportant, but because they are the safest to move.
The triage nurse is ActivityManager: it assigns the score.
The bed manager is lmkd: it acts on the score when the ward is full.
Where the analogy breaks#
A discharged patient goes home. A killed Android process does not go anywhere: it is destroyed, and its state must have been saved beforehand. That is why the lifecycle callbacks exist — they are the hospital's discharge paperwork, and an app that does not fill them in loses its state.
Also, moving a patient is expensive. Restarting an Android process means forking from Zygote and re-running
Application.onCreate, which is why the system avoids killing processes it might need again soon.
The big picture#
Text description of this diagram
An interactive ladder of app processes ordered by importance. Opening Browser makes it foreground and pushes the previous app down to cached. Pressing Home leaves the app cached but alive, with its process still in memory. Starting music raises Music to a service, above every cached app. Showing a dialog makes the app visible. As the working set passes the memory limit shown by the bar, lmkd kills cached processes from the bottom of the ladder, logging each kill. The persistent SystemUI and Launcher processes are never killed.
Work the buttons in order and watch two things: the adj tier of each app, and the kill order. Cached apps die first; a foreground app is never chosen.
Deep dive#
The importance tiers#
ActivityManager computes an oom_adj value for every process, and it recomputes it whenever anything relevant changes. The exact numbers have shifted across releases, so treat the tiers as the stable part:
| Tier | When | Killable? |
|---|---|---|
| Foreground | the app is doing something the user can see and interact with | effectively never |
| Visible | visible but not focused — behind a dialog, in split screen without focus | reluctantly |
| Perceptible | doing something the user perceives — playing audio, running a foreground service | reluctantly |
| Service | running a background service | yes, under pressure |
| Cached | not doing anything; kept only to make a return fast | first |
The exact adj values and their names have changed repeatedly. The tier names are the durable mental model; the numbers are an implementation detail and should be read from the device rather than memorised.
lmkd#
The low-memory killer daemon is a small native process, started by init, that watches memory pressure and kills processes when the device runs out.1
Its placement matters. The in-kernel OOM killer runs only when allocation fails — too late, and with no idea which process is important. lmkd is triggered earlier, by pressure thresholds, and it knows each process's adj. The result is a kill that happens before the system is thrashing rather than after.
lmkd can use PSI (Pressure Stall Information) as its signal.2 PSI reports, per resource, how much time tasks spent stalled waiting for memory, CPU or IO. A rising memory stall percentage means the system is already paying for the shortage; killing a cached process frees pages and drops the stall.
The freezer#
Killing is the last resort. Before that, Android can freeze a cached process: stop it from running without destroying it. A frozen process uses no CPU and cannot wake up on its own, but its memory stays resident and its state is intact, so a later unfreeze is much cheaper than a restart.
This is the mechanism behind "cached but frozen" apps that resume instantly. It is a middle tier between running and dead.
Why clearing recents does not help#
The intuition is that closing apps frees memory and saves battery. In practice:
- A cached process is already frozen or nearly idle. It consumes memory, not CPU.
- Killing it means the next launch must re-fork, re-run
Application.onCreate, and re-inflate the UI — which costs more CPU and battery than keeping it. - The Linux page cache that holds the app's code is also freed, so relaunching has to read from storage.
The system already kills cached processes when it needs the memory. A user swiping them away mostly defeats the system's own optimisation.
The restart path#
When a killed app is relaunched, the system does not restore a memory snapshot. It:
- Forks a new process from Zygote.
- Creates the
Applicationand runsonCreate. - Recreates the Activity and calls
onCreatewith a savedBundle. - Restores the view hierarchy from the saved state.
Everything the app did not save is gone. This is why onSaveInstanceState, ViewModel and SavedStateHandle exist:
they are the difference between "the user comes back to the same screen" and "the user comes back to a blank form".
Importance is not the same as execution quota#
A cached process can still be killed for reasons unrelated to memory: the background execution limits stop its services, App Standby restricts its jobs, and Doze defers its alarms. The OOM ladder is about memory; the background limits are about time. They act independently, and an app can be killed by either.
The freezer in practice#
A frozen process is in a kernel state where it cannot run, but its memory is intact. Unfreezing is fast — no fork, no
Application.onCreate, no re-inflation — which is why a cached app often returns instantly even after hours.
The freezer changes the OOM calculation. A process that is frozen and unlikely to be unfrozen soon is a cheap kill; one that is frozen but likely to return is worth keeping. The system's job is to guess which is which from recency, the standby bucket and the app's own behaviour.
What the app can do about it#
An app cannot raise its own adj. It can, however:
- Keep a foreground service for work the user must know about, which raises its tier and shows a notification.
- Respond to
onTrimMemoryso it is a cheaper neighbour, and therefore less likely to be chosen. - Save state before backgrounding, so a kill is invisible to the user.
- Avoid being the process that wakes up on a timer, which is what makes an app a candidate for the bottom of the ladder in the first place.
The last one is the important one. Most "my app keeps getting killed" reports are apps that wake themselves up frequently, which is exactly what the system is designed to suppress.
Why the ladder is not a queue#
Kills do not happen in strict order. lmkd picks the lowest-adj process that will free enough memory; if the lowest is tiny, it may kill several, and if the pressure is severe it may reach higher tiers.
That is why a cached app with a large memory footprint can be killed before a smaller one: the goal is freeing pages, not punishing apps.
The process death contract#
Android's contract is that an app's process can be killed at any time when it is not visible. Everything the app relies on must therefore survive one of three events:
- Configuration change: a
ViewModelsurvives. - Process death: saved instance state survives.
- Device reboot: only persisted data survives.
An app that stores user input only in a field's text and never in a ViewModel or saved state loses it on a kill —
and the user sees a form that has reset itself, with no explanation.
How an app should respond to each tier#
| Tier | What the app can do |
|---|---|
| Foreground | anything, including a long-running operation on a background thread |
| Visible | keep the UI live, avoid heavy work that could block the frame |
| Perceptible | keep playing audio or tracking, drop everything else |
| Service | finish quickly or promote to foreground; the window is short |
| Cached | save state; assume the process can die at any moment |
The table is the contract behind the lifecycle callbacks. onStop is not a signal to start work — it is a signal that
the process may be killed.
Why the ranking exists at all#
The alternative is a first-come-first-served memory allocator, which is what a desktop does: when memory runs out, something fails, and the user sees an out-of-memory dialog.
Android cannot ask the user to close an app. The OOM ladder is the mechanism that replaces that dialog with an invisible decision the user never has to make.
The cost of a wrong decision#
Killing the wrong process is expensive in a way that is easy to underestimate: the app must be restarted, which means
re-running Application.onCreate, re-inflating views and re-fetching data. On a slow device that can be a second or
more of visible latency when the user returns.
That is why the system is conservative about killing visible and perceptible processes, and why cached processes — which cost the least to lose and the most to keep — are the first choice.
How it evolved#
The sequence matters more than the dates:
- An in-kernel killer decided alone, with no knowledge of which app was in the foreground.
- lmkd moved the decision to userspace, where every process's adj is known.
- The freezer added a state between running and dead, so a cached app could be paused without being destroyed.
- Pressure signals let lmkd react to stalls rather than to a memory threshold being crossed.
| API | Version | What changed |
|---|---|---|
| 1 | 1.0 |
|
| 8 | 2.2 Froyo |
|
| 9 | 2.3 Gingerbread |
|
| 19 | 4.4 KitKat |
|
| 21 | 5.0 Lollipop |
|
| 23 | 6.0 Marshmallow |
|
| 24 | 7.0 Nougat |
|
| 33 | 13 Tiramisu |
|
| 37 | 17 Cinnamon Bun |
|
Under the hood#
- lmkd.cppthe daemon: pressure monitoring and the kill loop
- OomAdjuster.javawhere adj is computed from process state
- ProcessList.javathe memory limits and the kill requests
- ActivityManager.javathe public view of importance and memory classes
Curiosity corner#
- The killer used to be in the kernel. Android had an out-of-tree low-memory killer before lmkd; it was replaced because policy belongs where the app state is known.
- A process can be important without being visible. A music player with a foreground service is perceptible and survives pressure that kills a visible-but-idle app.
- The adj of a process can change many times a second during a transition, which is why traces show it moving rather than sitting at one value.
Try it#
adb shell dumpsys activity processes | grep -i "Proc #"Example output
Proc # 0: fg T/A/TOP LCMN 17382:com.example.app/u0a123
Proc # 1: vis T/A/VIS LCMN 17411:com.android.systemui/u0a45
Proc # 2: cch T/A/CAC LCMN 17502:com.example.other/u0a130adb shell logcat -b events -s lmkdExample output
lmkd: kill(17502, 17502, 0, 0)adb shell cat /proc/pressure/memoryExample output
some avg10=0.42 avg60=0.11 avg300=0.02 total=18422311
full avg10=0.38 avg60=0.09 avg300=0.01 total=16220874Check yourself#
Check yourself
Why can clearing apps from the recents screen fail to save battery?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsLow-memory killer daemon (lmkd)source.android.comaccessed 2026-10-27↩
- docsPressure Stall Information (PSI)docs.kernel.orgaccessed 2026-10-27↩
- docsProcesses and app lifecycledeveloper.android.comaccessed 2026-10-27↩
- docsManage your app's memory overviewdeveloper.android.comaccessed 2026-10-27↩
- docsBinder freezer and cached app freezingsource.android.comaccessed 2026-10-27↩
- docsdumpsysdeveloper.android.comaccessed 2026-10-27↩