Zygote
Memory, lifecycle & powerintermediate

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.

13 minAPI 1–37 · 17interactivequizverified 2026-10-27

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#

Loading diagram…
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:

TierWhenKillable?
Foregroundthe app is doing something the user can see and interact witheffectively never
Visiblevisible but not focused — behind a dialog, in split screen without focusreluctantly
Perceptibledoing something the user perceives — playing audio, running a foreground servicereluctantly
Servicerunning a background serviceyes, under pressure
Cachednot doing anything; kept only to make a return fastfirst

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:

  1. Forks a new process from Zygote.
  2. Creates the Application and runs onCreate.
  3. Recreates the Activity and calls onCreate with a saved Bundle.
  4. 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 onTrimMemory so 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 ViewModel survives.
  • 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#

TierWhat the app can do
Foregroundanything, including a long-running operation on a background thread
Visiblekeep the UI live, avoid heavy work that could block the frame
Perceptiblekeep playing audio or tracking, drop everything else
Servicefinish quickly or promote to foreground; the window is short
Cachedsave 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.
The transition from an in-kernel killer to a userspace one, and from fixed thresholds to PSI, is real; the dates are deliberately not pinned here.unverified(the exact release for each step varies by device and kernel; the ordering is the stable part)
Runtime changes9 releases
APIVersionWhat changed
11.0
  • Initial AOSP platform: Linux kernel, Dalvik VM, core libraries
82.2 Froyo
  • Trace-based JIT compiler added to Dalvik
92.3 Gingerbread
  • Concurrent garbage collector added to Dalvik (GC pause reduction)
194.4 KitKat
  • Android Runtime (ART) appears as an experimental runtime
215.0 Lollipop
  • ART becomes the only runtime: ahead-of-time compilation at install
236.0 Marshmallow
  • Runtime permission model changes how every app asks for access
247.0 Nougat
  • Hybrid JIT + AOT compilation in ART
3313 Tiramisu
  • The notification permission becomes a runtime permission
3717 Cinnamon Bun
  • Generational garbage collection is added to ART's Concurrent Mark-Compact collector

Under the hood#

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#

Processes by importance
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/u0a130
lmkd's view
adb shell logcat -b events -s lmkd
Example output
lmkd: kill(17502, 17502, 0, 0)
Pressure stall information
adb shell cat /proc/pressure/memory
Example output
some avg10=0.42 avg60=0.11 avg300=0.02 total=18422311
full avg10=0.38 avg60=0.09 avg300=0.01 total=16220874

Check yourself#

Check yourself

1 / 12

Why can clearing apps from the recents screen fail to save battery?

0 answered · 0 correct

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.

  1. docsLow-memory killer daemon (lmkd)source.android.comaccessed 2026-10-27↩
  2. docsPressure Stall Information (PSI)docs.kernel.orgaccessed 2026-10-27↩
  3. docsProcesses and app lifecycledeveloper.android.comaccessed 2026-10-27↩
  4. docsManage your app's memory overviewdeveloper.android.comaccessed 2026-10-27↩
  5. docsBinder freezer and cached app freezingsource.android.comaccessed 2026-10-27↩
  6. docsdumpsysdeveloper.android.comaccessed 2026-10-27↩