To understand memory usage on Android, you must look at the system from several different perspectives, ranging from high-level Java objects down to low-level kernel pages.
Memory use in and of itself is not a good or bad thing; what matters is what you are using the memory for. However, as you approach the limit of available memory on a device, you eventually “fall off a performance cliff.”
When you are far away from the cliff, adding a small amount of memory usage (e.g., 50MB) may have no perceptible impact on performance. However, once you reach the cliff, the operating system must start paging out memory and killing processes to free up space. At this point, even a small increase in memory use can lead to significant “thrashing,” where the device becomes unresponsive or appears to reboot because critical processes are killed.
Each layer of the Android stack has its own unique view of memory:
If you want to optimize memory, you must either optimize within your own layer (e.g., loading fewer bitmaps) or understand the layers below you to see how your high-level allocations translate to physical page usage.
Android minimizes the cost of starting new applications by using a process called Zygote.
As long as the child process only reads the memory inherited from Zygote, the physical memory pages remain shared between all processes. When a child process modifies a shared page, the kernel transparently creates a private copy of that page for the process. This model allows many processes to share a large portion of their memory—especially the framework code and resources—significantly reducing the overall system memory footprint.
Because memory is shared heavily between processes—primarily through the Zygote model—there are three main ways to account for a process's memory usage:
Note: None of these metrics (RSS, PSS, or USS) include pages that have been compressed and swapped into ZRAM, or pages that have been reclaimed by the kernel. Memory usage metrics on Android generally represent “resident” memory.
Before diving into how the OS reclaims memory, you must understand the two fundamental categories of memory pages in Linux:
anon): Memory that is not backed by a file on storage. This includes memory allocated by C/C++ malloc (like the Scudo allocator) and memory allocated for Java objects on the Dalvik heap. Because this memory has no source file to return to, the OS must either keep it in RAM or compress and swap it out to ZRAM when under pressure.file): Memory that is mapped directly from a file on storage. This includes executable code (DEX files, .so native libraries) and fonts.Android uses mmap to map both file-backed and anonymous memory into a process's address space.
When dealing with file-backed memory, pages are further classified into:
Android prefers file formats (like DEX) that are amenable to memory mapping, allowing the system to easily reclaim clean memory under pressure. Because anonymous and dirty memory cannot be simply dropped, high private dirty/anon memory is the primary cause of system thrashing and Low Memory Killer (LMK) interventions. If your application uses a lot of private dirty memory, you are directly contributing to the performance cliff.
Next: Quick Assessment Tools