androidengineers.Book a session

Android Architecture Fundamentals

Process & Application Lifecycle Deep Dive

article25 minMedium

Overview

Android is a memory-constrained, multi-app environment. The OS must kill processes to reclaim memory for the foreground app. Understanding how process priorities are calculated — and how your app can respond — is essential for writing apps that survive background pressure without leaking memory.


Process Priority Levels

Android assigns every app process a priority level that the Low Memory Killer (LMKD) uses to decide which process to kill first when memory is scarce.

1. Foreground Process

The highest priority. A process is foreground if it has:

  • An Activity in RESUMED state (user can see it and interact with it)
  • A Service running in the foreground (via startForeground())
  • A BroadcastReceiver currently executing onReceive()

Foreground processes are killed only as an absolute last resort.

2. Visible Process

A process whose Activity is visible but not focused:

  • Activity is PAUSED (e.g., covered by a dialog from another app, or split-screen adjacent)
  • A Service bound to a visible Activity

These processes are very important to the user experience and are kept alive as long as possible.

3. Service Process

A process running a Service started via startService() that is not foreground. Examples: music playback service (before calling startForeground), sync service.

These have been running for some time and their data is important, so Android tries to keep them alive. But under memory pressure they are killed before visible processes.

4. Cached (Background) Process

An Activity that is stopped (user navigated away). The process is in the LRU cache. Android kills these in LRU order — least recently used first.

5. Empty Process

A process with no active components. Kept only to improve cold-start time on next launch. First to be killed.


oom_adj Score

Each process is assigned an integer oom_adj score. Lower score = higher priority = less likely to be killed.

# View oom_adj for all processes
adb shell cat /proc/<pid>/oom_adj
# Or for all at once:
adb shell dumpsys activity processes | grep oom_adj

Typical ranges (can vary by device/kernel):

Priorityoom_adj range
Foreground-17 to 0
Visible1 to 99
Service100 to 199
Cached/background200 to 906
Empty906+

LMKD kills processes from highest oom_adj down until enough memory is freed. You cannot control oom_adj directly; it is computed by ActivityManagerService based on component state.


Application Class Callbacks

Your Application subclass receives memory pressure callbacks. Use them to drop caches and free resources.

class MyApplication : Application() {

    override fun onLowMemory() {
        // Deprecated in API 14 but still called for compatibility.
        // Equivalent to TRIM_MEMORY_COMPLETE.
        Glide.get(this).clearMemory()
        clearInMemoryCaches()
    }

    override fun onTrimMemory(level: Int) {
        when (level) {
            // App is running, but system is low on memory.
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE -> {
                // Start releasing non-critical caches (e.g., thumbnail cache)
            }
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW -> {
                // Release more. You are at risk of being killed.
                Glide.get(this).onTrimMemory(level)
            }
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                // Release everything you can. You will soon be killed.
                clearAllCaches()
            }

            // App was backgrounded.
            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                // Your UI has become hidden. Good time to release
                // UI-related caches (bitmaps cached for animation, etc.)
                releaseBitmapCache()
            }

            // App is in the background LRU list.
            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> {
                // Begining of LRU list. Release a small amount.
            }
            ComponentCallbacks2.TRIM_MEMORY_MODERATE -> {
                // Middle of LRU list. Release more.
            }
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                // Near the end of the LRU list. Release everything possible.
                clearAllCaches()
            }
        }
    }
}

onTrimMemory() is also called on Activity, Service, and Fragment — not just Application.


Activity Task & Back Stack

A Task is a stack of Activities the user has navigated through. The device Back button (or predictive back gesture) pops the top Activity.

Task A (foreground)
 ├── MainActivity       ← bottom of stack
 ├── DetailActivity
 └── EditActivity       ← top (user is here)

Multiple tasks can exist simultaneously (e.g., user switches apps). The most recently used task's process gets a lower oom_adj score (higher priority).

Launch modes and their effect on the stack

// In AndroidManifest.xml
<activity
    android:name=".DetailActivity"
    android:launchMode="singleTop" />

// Or via Intent flags:
val intent = Intent(this, DetailActivity::class.java).apply {
    flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
}
Launch modeBack stack behavior
standard (default)New instance always created
singleTopReuses existing top instance; calls onNewIntent()
singleTaskOne instance per task; existing instance brought to top
singleInstanceIsolated in its own task

How Process Death Affects State

When Android kills your process (or the user force-stops it), all in-memory state is lost. The Activity lifecycle gives you hooks to save and restore UI state:

class DetailActivity : AppCompatActivity() {

    private var scrollPosition: Int = 0

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        // Called before the Activity may be destroyed.
        // NOT called when the user presses Back (intentional finish).
        outState.putInt("SCROLL_POS", scrollPosition)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_detail)

        // savedInstanceState is non-null only when recreating from saved state.
        if (savedInstanceState != null) {
            scrollPosition = savedInstanceState.getInt("SCROLL_POS", 0)
        }
    }

    override fun onRestoreInstanceState(savedInstanceState: Bundle) {
        super.onRestoreInstanceState(savedInstanceState)
        // Called after onCreate() if state was saved. Alternative restoration point.
        scrollPosition = savedInstanceState.getInt("SCROLL_POS", 0)
    }
}

ViewModel survives configuration changes, not process death

class DetailViewModel : ViewModel() {
    // Survives rotation.
    // Does NOT survive process death.
    var data: List<Item> = emptyList()
}

// For process-death survival, use SavedStateHandle:
class DetailViewModel(private val savedState: SavedStateHandle) : ViewModel() {
    var query: String
        get() = savedState["query"] ?: ""
        set(value) { savedState["query"] = value }
}

Verifying Process Death in Development

# Simulate the system killing your background process:
adb shell am kill com.example.app

# Force-stop (like Settings > Force Stop):
adb shell am force-stop com.example.app

# Enable "Don't keep activities" in Developer Options
# — destroys every Activity as soon as user leaves it,
#   excellent for testing onSaveInstanceState coverage.

Practical Gotchas

  • Never store state in Application fields and assume it survives. On process re-launch after death, Application.onCreate() is called fresh. Check for null everywhere.
  • onSaveInstanceState is not called when the user presses Back. Use onPause() / onStop() to persist state to a database for intentional exits.
  • Foreground service does not make your process immune. Under extreme memory pressure, even foreground processes can be killed (rare). Persist critical state to disk.
  • Worker processes (:remote in manifest) have a separate PID and their own lifecycle. Crashes in worker processes do not crash the main process.
  • onTrimMemory(TRIM_MEMORY_UI_HIDDEN) is the right place to drop bitmap caches — not onPause() which fires every time you show a dialog.
  • singleTask + deep links — if your app is already running and receives a deep link intent, the existing instance gets onNewIntent(). Failing to handle this causes the deep link to be silently swallowed.

Summary

PriorityTriggerKilled when
ForegroundResumed Activity or foreground ServiceAlmost never
VisiblePaused Activity or bound to visibleRarely
ServiceBackground started ServiceModerate pressure
CachedStopped Activity in LRURegular background pressure
EmptyNo componentsFirst to go

Design your app so that every screen can be recreated from scratch — either from a database, SavedStateHandle, or network — because the OS will eventually kill your process when you are in the background.

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Process & Application Lifecycle Deep Dive | Android System Design | Android Engineers