Imagine writing an application and hardcoding explicit hardware memory addresses. If two processes asked for the exact same physical address, they would collide and crash the operating system immediately.
To solve this, the kernel gives each process a fake, isolated address space called virtual memory. Memory is divided into 4KB chunks called pages. When a Java or C++ process asks for 1MB of memory, the kernel does not hand over physical RAM chips immediately. Instead, it maps virtual memory addresses to physical locations using page tables.
If a process attempts to read a virtual page that has not been mapped to physical RAM yet, the CPU triggers a page fault. The kernel pauses the process, rapidly allocates a physical block of RAM, updates the page table, and resumes the execution. This lazy allocation strategy is known as demand paging. Applications believe they have infinite, contiguous memory, but the kernel manages the physical reality behind the scenes.
You can view how much memory your Android system is actively managing at a granular level. The kernel exposes its memory information file directly to the shell. This command dumps the current state of virtual and physical allocations into the terminal.
adb shell cat /proc/meminfo
This output reveals values like MemTotal, MemFree, and Cached. A common mistake engineers make is looking only at MemFree to determine available memory. They completely forget that the kernel uses the Cached pool aggressively to keep file data in RAM. Android will instantly drop those caches when an application needs real memory.
But how does the kernel quickly find contiguous blocks of free RAM when millions of pages are constantly being allocated and freed? As applications launch and exit, memory naturally becomes peppered with tiny free gaps. If the system needs a contiguous 32KB block but only has scattered 4KB holes available, the allocation fails despite having enough total free memory.
To combat this fragmentation, the kernel uses the Buddy Allocator algorithm. The system tracks available physical pages by grouping them into power-of-two blocks like 1, 2, 4, or 8 pages. If a process requires 2 pages but only an 8-page block is available, the allocator splits the 8-page block into two 4-page buddies. It then splits one of those 4-page buddies into two 2-page buddies and allocates one of them.
When that memory is eventually freed, the allocator attempts to merge the buddies back into larger contiguous blocks. The following flowchart illustrates how the buddy allocator mathematically halves large blocks to serve smaller requests. This visual breakdown helps explain why power-of-two sizing makes splitting and merging highly efficient. Follow the split decisions from the initial 8-page block down to the allocated 2-page buddy.
The kernel takes a large block and halves it repeatedly until it matches the requested size perfectly. This systematic approach minimizes severe fragmentation. Keeping memory contiguous allows a smartphone to run for months without requiring a reboot.
Eventually, the device simply runs out of physical RAM entirely. When a traditional Linux desktop runs low on memory, it moves idle pages to a swap file on a fast storage drive. Smartphones cannot do this safely. Constant background swapping to mobile flash storage would drastically reduce the lifespan of the memory chip and drain the battery.
Android solves this hardware constraint by disabling traditional hard drive swapping entirely. Instead, it relies on the Low Memory Killer daemon. The ActivityManagerService assigns an Out-Of-Memory adjustment score to every running process. These oom_score_adj values range from -1000 for critical system services to 1000 for cached background apps.
When the kernel detects that free physical RAM has dropped below a critical threshold, it wakes up the lmkd process. This user-space daemon scans the process list for the application with the highest score. It then instantly sends a SIGKILL signal to that process. The target application terminates without warning, immediately relinquishing its memory pages.
The sequence diagram below shows the exact assassination flow between the kernel and the killer daemon. Seeing this interaction clarifies why Android apps cannot rely on standard shutdown callbacks when memory runs out. Notice how the targeted application receives absolutely no warning before termination.
The kernel detects severe memory pressure and delegates the execution to user space. Lmkd violently terminates the cached application and returns its physical pages back to the Buddy Allocator.
This brutal but necessary mechanism explains why your large 3D game reloads from scratch after you briefly switch to a heavy camera application. The system decided your game was the most expendable target in memory. Android executed the background process to keep the foreground experience fast and responsive.