Android 17 and higher supports the memory limiter, which is a system service that monitors and limits the memory usage of application processes using Linux cgroup v2. The Memory Limiter prevents individual apps from consuming excessive system memory, which reduces system-wide memory pressure and prevents aggressive out-of-memory (OOM) killing of critical processes.
Mechanism
The Memory Limiter integrates with the Activity Manager Service (AMS) to track process lifecycle events and state changes. The Memory Limiter enforces memory limits using the Linux kernel cgroup v2 file system.
To use the Memory Limiter, the device kernel must support cgroup v2 and the
memory controller. The service specifically relies on the following
attributes:
memory.high- A soft limit. When exceeded, the process is throttled and the kernel attempts to reclaim memory from it.
memory.swap.max- Limits the amount of swap space the process can use.
Impact on apps
Apps that don't exceed their memory limits aren't affected by the Memory Limiter.
When an app crosses its memory.high limit, the kernel evicts the app's
file-backed memory and swaps out its anonymous memory to keep the app within the
limit. As a result of eviction and swap, the app might run slower.
At the extreme, if the app continues allocating anonymous memory and the device runs out of swap space, the app might fail to allocate memory, and as a result is likely to crash.
Process monitoring
The Memory Limiter monitors app processes (UID >= 10000) by default. System processes are generally exempt to help verify core system stability.
The Memory Limiter assigns memory limits based on the state of the process:
Visible processes are perceptible to the user, such as foreground activities, foreground services, or other jank-perceptible states.
Not visible processes are background processes that aren't interacting with or visible to the user.
The following table maps specific process states to memory limits:
| Process state | Memory limit |
|---|---|
PERSISTENT | Unrestricted |
PERSISTENT_UI | Unrestricted |
TOP | Visible |
BOUND_TOP | Visible |
FOREGROUND_SERVICE | Not visible |
BOUND_FOREGROUND_SERVICE | Not visible |
IMPORTANT_FOREGROUND | Visible |
IMPORTANT_BACKGROUND | Not visible |
TRANSIENT_BACKGROUND | Not visible |
BACKUP | Not visible |
SERVICE | Not visible |
RECEIVER | Not visible |
TOP_SLEEPING | Visible |
HEAVY_WEIGHT | Not visible |
HOME | Not visible |
LAST_ACTIVITY | Not visible |
CACHED_ACTIVITY | Cached |
CACHED_ACTIVITY_CLIENT | Cached |
CACHED_RECENT | Cached |
CACHED_EMPTY | Cached |
In the cached state, processes are frozen and then maximally reclaimed.
When a process exceeds its assigned memory.high limit, the Memory Limiter
detects the event and can trigger debugging actions, such as capturing a memory
profile or logging an anomaly to statsd.
Configuration
Configure the Memory Limiter using an XML file located on the vendor
partition. Configuration lets you tune absolute memory limits based on the
specific memory constraints of the device.
File path:
/vendor/etc/memory-limiter-config.xmlDefault configuration: If the configuration file isn't found, or if it's unreadable or invalid, the Memory Limiter is disabled.
XML format
The configuration file follows the schema defined in
memory-limiter-config.xsd. The file lets you define multiple limit sets; the
service chooses the best match based on the device's available RAM. All memory
values are defined in units of mebibytes (MiB).
<MemoryLimiterConfig>
<version>1</version>
<configList>
<limitSet>
<!-- Limits for a phone with at least 14G of ram: 8G/4G/4G/4G -->
<minimumRequiredMemTotal>14336</minimumRequiredMemTotal>
<memVisible>8192</memVisible>
<memNotVisible>4096</memNotVisible>
<swapVisible>4096</swapVisible>
<swapNotVisible>4096</swapNotVisible>
</limitSet>
</configList>
</MemoryLimiterConfig>
version- A positive integer identifying the configuration version. This must be 1.
minimumRequiredMemTotal- The minimum required available system memory for this limit set to be valid.
memVisible- The memory limit (
memory.high) allowed for visible processes. memNotVisible- The memory limit (
memory.high) allowed for not visible processes. swapVisible- The swap limit (
memory.swap.max) allowed for visible processes. swapNotVisible- The swap limit (
memory.swap.max) allowed for not visible processes.
Modify configuration
To change system-wide limits, follow these steps:
- Modify
/vendor/etc/memory-limiter-config.xml. - Reboot the device or restart
system_serverfor the changes to take effect.
Shell commands
The am memory-limiter command lets you and developers interact with the
service at run time for development and testing:
am memory-limiter <SUB-COMMAND>status
The status sub-command reports the operational status of the Memory Limiter:
adb shell am memory-limiter statusExample output:
Memory limiter
enabled monitoring=true ignored=none
visibleMem=1948MB visibleSwap=974MB
notVisibleMem=974MB notVisibleSwap=487MB
started=36 watched=36 watch-failed=0
events=0 processes=36 process-hwm=36
Key fields in the output include:
monitoring- Indicates whether the limiter is actively watching processes.
visibleMemandnotVisibleMem- Indicate the calculated absolute memory limits for each state.
events- The number of times a process has exceeded its limit.
processes- The number of monitored processes.
ignore
The ignore sub-command temporarily excludes a UID or all processes from being
limited. This action is useful for performance testing or when allowing a
specific app to exceed its limits.
adb shell am memory-limiter ignore 10087 // Ignore a specific UIDadb shell am memory-limiter ignore all // Ignore all processes (effectively disables limiting)adb shell am memory-limiter ignore none // Resume normal operation
manual
The manual sub-command overrides the calculated limits for a specific process
(by process ID, or PID) with a custom absolute value in megabytes (MB):
adb shell am memory-limiter manual 1234 1024 // Set a 1024 MB limit for PID 1234adb shell am memory-limiter manual 1234 none // Remove the manual override for PID 1234
Manual overrides apply only to the lifecycle of the process. If the process restarts, it returns to the default limits based on its state.