CN

Overview of Jank and Frame Drops in Android - Low Memory

Word count: 3.5kReading time: 21 min
2019/09/18
loading

In the Overview of Jank and Frame Drops in Android - System Layer article, we touched upon jank cases caused by low system memory. Since low memory has a significant impact on overall device performance, I’m dedicating this article to exploring those effects in depth.

As Android versions evolve and apps become more feature-heavy, their memory requirements increase. When this article was written in 2019, many devices had 4GB of RAM or less. These users are prone to low memory situations, especially after major system updates or as more apps and services run concurrently.

Memory pressure can contribute to slow responsiveness and jank. Examples include longer app launch times, more frame drops in scrolling, more frequent cold starts as background processes are killed, and extra reclaim/restart work that can increase power use. Heat alone does not diagnose memory pressure. Below, I’ll outline the causes, debugging methods, and potential optimizations for these issues.

Version note — technical review, 2026-10-13: This article retains 2019 logs and Systrace cases. Old kernel LMK logs, tuning interfaces, and function names have version limits. Updates below cover Android 10/11 PSI/lmkd, Android 16 framework code, and Linux 6.6 reclaim mechanisms. Complete original traces and controlled experimental conditions are unavailable; the review corrects visible evidence rather than presenting a new device benchmark.

  1. Overview of Jank and Frame Drops in Android - Methodology
  2. Overview of Jank and Frame Drops in Android - System Layer
  3. Overview of Jank and Frame Drops in Android - Application Layer
  4. Overview of Jank and Frame Drops in Android - Low Memory

Identifying Low Memory

Meminfo Data

Start with Android’s system summary, then correlate processes and changes over time:

1
adb shell dumpsys meminfo

The original output is preserved below. 156,ba480K is corrupted text, not a valid number to reconstruct by guessing.

1
2
3
4
5
6
7
......
Total RAM: 7,658,060K (status moderate)
Free RAM: 550,200K ( 78,760K cached pss + 156,ba480K cached kernel + 314,960K free)
Used RAM: 7,718,091K (6,118,703K used pss + 1,599,388K kernel)
Lost RAM: -319,863K
ZRAM: 2,608K physical used for 301,256K in swap (4,247,544K total swap)
Tuning: 256 (large 512), oom 322,560K, restore limit 107,520K (high-end-gfx)

This is not a universal low-memory signature:

  • In Android 9 AMS, Free RAM combines cached-process PSS, some kernel caches, and actually free pages. It differs from both MemFree and MemAvailable in /proc/meminfo.
  • Used RAM and Lost RAM use different accounting involving PSS, swap PSS, kernel, and ZRAM values. Here, Used exceeds Total and Lost is negative. Check the system branch and original samples; do not interpret this as physically negative memory or proof of a leak. status moderate is also a framework state, not a per-frame pressure measurement.
  • 2,608K physical used, 301,256K in swap, and 4,247,544K total swap describe physical consumption, used swap space, and swap capacity. They are not interchangeable ZRAM utilization measures. These figures alone do not support the original claim of very high ZRAM use.

Caches are useful. Look for sustained loss of available memory, stalls from reclaim or swap-in, and foreground responsiveness in the same interval. A single low-free-memory snapshot is insufficient.

LMK and kswapd Activity

This is a historical in-kernel lowmemorykiller driver log, not the universal format of modern userspace lmkd:

1
2
3
4
5
6
7
8
9
[kswapd0] lowmemorykiller: Killing 'u.mzsyncservice' (15609) (tgid 15609), adj 906,
to free 28864kB on behalf of 'kswapd0' (91) because
cache 258652kB is below limit 261272kB for oom score 906
Free memory is -5540kB above reserved.
Free CMA is 3172kB
Total reserve is 227288kB
Total free pages is 271748kB
Total file cache is 345384kB
GFP mask is 0x14000c0

It records selecting PID 15609, adj 906, in kswapd0‘s reclaim context, with cache 258652kB, threshold 261272kB, and other conditions. It does not establish a universal “adj 900 watermark.” -5540kB above reserved is a value after reserve accounting, not a negative physical free-page count. The exact selection policy requires that vendor’s kernel source.

kswapd performs background page reclaim. The old LMK driver and userspace lmkd are separate implementations; neither should be conflated with the Linux OOM killer or AMS cached/empty-process trimming. Active kswapd alone does not prove that a process was killed.

Version update: The AOSP lmkd guide describes PSI support in Android 10 and the Android 11 strategy’s additional consideration of memory use and page-cache thrashing. Actual operation depends on kernel support and product configuration. Use that version’s lmkd reasons and pressure data rather than searching only for the old lowmemorykiller: prefix.

/proc/meminfo

Read the kernel summary with the leading slash in the path:

1
adb shell cat /proc/meminfo

This original sample has a different MemTotal from the dumpsys sample. Do not treat them as one synchronized device snapshot.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
MemTotal:        5630104 kB
MemFree: 148928 kB
MemAvailable: 864172 kB
Buffers: 28464 kB
Cached: 1003144 kB
SwapCached: 19844 kB
Active: 1607512 kB
Inactive: 969208 kB
Active(anon): 1187828 kB
Inactive(anon): 426192 kB
Active(file): 419684 kB
Inactive(file): 543016 kB
Unevictable: 62152 kB
Mlocked: 62152 kB
SwapTotal: 2097148 kB
SwapFree: 42576 kB
Dirty: 3604 kB
Writeback: 0 kB
AnonPages: 1602928 kB
Mapped: 996768 kB
Shmem: 7284 kB
Slab: 306440 kB
SReclaimable: 72320 kB
SUnreclaim: 234120 kB
KernelStack: 89776 kB
PageTables: 107572 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 4912200 kB
Committed_AS: 118487976 kB
VmallocTotal: 263061440 kB
VmallocUsed: 0 kB
VmallocChunk: 0 kB
CmaTotal: 303104 kB
CmaFree: 3924 kB

The Linux 6.6 /proc documentation distinguishes free RAM (MemFree) from an estimate of memory available to new applications without swapping (MemAvailable), accounting for reclaimable caches and reserves. The values here are 148928 kB and 864172 kB; they are not both almost zero. SwapFree of 42576 kB shows heavily used swap space, not necessarily active swap churn at this instant. Committed_AS is not physical residency.

Where the kernel provides the interfaces and permissions allow them, further read-only observations include:

1
2
3
4
adb shell cat /proc/vmstat
adb shell cat /proc/pressure/memory
adb shell cat /proc/pressure/io
adb shell cat /proc/swaps

Use deltas around a reproduction for reclaim scans/successes, allocstall*, pswpin/pswpout, and workingset_refault*; field names vary by kernel. Large lifetime totals do not date a stall. PSI some measures time with at least some tasks stalled; full measures simultaneous stalls of all non-idle tasks. avg10/60/300 are time percentages over those windows in seconds; total is cumulative microseconds. See the PSI documentation. Missing interfaces, denied access, or uncaptured data mean unavailable, not zero pressure.

System-wide Lag & Slow Response

Memory pressure can contribute to slow launches, dropped frames, and more cold starts, but CPU, storage, locks, and other loads can cause similar symptoms. Align the user-visible delay with pressure, reclaim, thread waits, and process-exit evidence before placing memory on the critical path.

Impact of Low Memory on Performance

Main Thread I/O Latency

Pressure adds reclaim work, may evict file pages needed soon afterward, and may cause swap-related work. Foreground latency grows if the thread then waits for pages or storage completion.

Distinguish plain Uninterruptible Sleep from subclasses tagged Block I/O, and inspect the blocking call site. D state or a yellow color alone does not establish disk I/O. wait_on_page_bit_killable indicates a page-state wait, not by itself low memory, a specific file, or saturated storage. Cached reads need not block this way; loading a View does not necessarily read storage.

The SystemUI screenshot shows io_schedule as its blocking call site. That supports investigating I/O waits, but supplies no simultaneous memory-pressure value:

SystemUI Block I/O state with an io_schedule blocking call site

The next screenshot shows OPScreenRecord in bindApplication, including APK/OAT loading. Its thread states suggest waits during startup; color alone does not identify every wait’s cause.

OPScreenRecord bindApplication and APK/OAT loading intervals

CPU Contention

Reclaim, compression/decompression, and reloading content consume CPU. They can increase foreground Runnable delays when competing for execution time; confirm that on scheduling tracks instead of inferring it from a thread name.

The crop below shows kswapd0 repeatedly running on CPU 7. The original article described a Snapdragon 855 device, but this crop lacks enough topology and numerical frequency data to prove “the prime core at maximum frequency.” It also does not measure power or a specific foreground thread’s delay.

Repeated kswapd0 execution on CPU 7; no readable numerical frequency scale

HeapTaskDaemon processes ART heap tasks. Its activity is neither automatically all GC nor all stop-the-world time. Correlate actual heap/GC tasks and allocation activity before attributing it to system pressure.

Process Death-Restart Cycles

Repeated exits and restarts add allocation, code-loading, and I/O work. First identify both the kill and restart reasons. The original Monkey-test log is preserved:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
07-23 14:32:16.932  1435  1510 I am_proc_start: [0,30387,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:16.969 1435 3420 I am_proc_bound: [0,30387,com.tencent.mobileqq]
07-23 14:32:16.979 1435 3420 I am_kill : [0,30387,com.tencent.mobileqq,901,empty #3]
07-23 14:32:16.996 1435 3420 I am_proc_died: [0,30387,com.tencent.mobileqq,901,18]
07-23 14:32:17.028 1435 1510 I am_proc_start: [0,30400,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.054 1435 3420 I am_proc_bound: [0,30400,com.tencent.mobileqq]
07-23 14:32:17.064 1435 3420 I am_kill : [0,30400,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.082 1435 3420 I am_proc_died: [0,30400,com.tencent.mobileqq,901,18]
07-23 14:32:17.114 1435 1510 I am_proc_start: [0,30413,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.139 1435 3420 I am_proc_bound: [0,30413,com.tencent.mobileqq]
07-23 14:32:17.149 1435 3420 I am_kill : [0,30413,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.166 1435 3420 I am_proc_died: [0,30413,com.tencent.mobileqq,901,18]
07-23 14:32:17.202 1435 1510 I am_proc_start: [0,30427,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.216 1435 3420 I am_proc_bound: [0,30427,com.tencent.mobileqq]
07-23 14:32:17.226 1435 3420 I am_kill : [0,30427,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.249 1435 3420 I am_proc_died: [0,30427,com.tencent.mobileqq,901,18]
07-23 14:32:17.278 1435 1510 I am_proc_start: [0,30440,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.299 1435 3420 I am_proc_bound: [0,30440,com.tencent.mobileqq]
07-23 14:32:17.309 1435 3420 I am_kill : [0,30440,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.329 1435 2116 I am_proc_died: [0,30440,com.tencent.mobileqq,901,18]
07-23 14:32:17.362 1435 1510 I am_proc_start: [0,30453,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.387 1435 2116 I am_proc_bound: [0,30453,com.tencent.mobileqq]
07-23 14:32:17.398 1435 2116 I am_kill : [0,30453,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.420 1435 2116 I am_proc_died: [0,30453,com.tencent.mobileqq,901,18]
07-23 14:32:17.447 1435 1510 I am_proc_start: [0,30466,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.474 1435 2116 I am_proc_bound: [0,30466,com.tencent.mobileqq]
07-23 14:32:17.484 1435 2116 I am_kill : [0,30466,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.507 1435 2116 I am_proc_died: [0,30466,com.tencent.mobileqq,901,18]
07-23 14:32:17.533 1435 1510 I am_proc_start: [0,30479,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.556 1435 2116 I am_proc_bound: [0,30479,com.tencent.mobileqq]
07-23 14:32:17.566 1435 2116 I am_kill : [0,30479,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.587 1435 2116 I am_proc_died: [0,30479,com.tencent.mobileqq,901,18]
07-23 14:32:17.613 1435 1510 I am_proc_start: [0,30492,10145,com.tencent.mobileqq,restart,com.tencent.mobileqq]
07-23 14:32:17.636 1435 2116 I am_proc_bound: [0,30492,com.tencent.mobileqq]
07-23 14:32:17.646 1435 2116 I am_kill : [0,30492,com.tencent.mobileqq,901,empty #3]
07-23 14:32:17.667 1435 2116 I am_proc_died: [0,30492,com.tencent.mobileqq,901,18]

All nine am_kill events say empty #3. Android 9 AMSuses empty # plus a count when trimming excess empty processes; Android 16 OomAdjusterstill has the corresponding too many empty path. This supports repeated framework trimming followed by restarts, not a direct LMK-killed-QQ conclusion. The restart label alone does not identify a parent process reviving the app; investigate component/service restarts and other launch requests.

The corresponding system_server screenshot shows repeated start and killProcessGroup activity. That shape is not exclusive to any one kill reason:

Repeated process starts and killProcessGroup activity in system_server

The CPU crop below shows dense execution slices, but lacks complete per-thread accounting. It cannot attribute all CPU work to QQ or LMK. These are historical observations, not judgments about current app versions.

Dense CPU execution slices in the historical trace

Memory Reclamation and I/O Triggers

After a phone has been in use for a long time, opening an app you used earlier can feel slower than opening it for the first time: you have to wait for it to load again. That is one of the situations that led me to look into low-memory jank. The symptom alone does not tell us the cause, though. We need to separate these mechanisms:

  • The allocation fast path tries available free pages; it is not “fast-path reclaim.” On failure, the slow path may wake kswapd, reclaim directly, or compact memory depending on flags and conditions. Direct reclaim runs in the allocating thread’s context and can affect foreground latency. See Linux 6.6 page_alloc.c.
  • File-page reclaim does not always write storage. Eligible clean file-cache pages can be discarded and read again if needed; dirty pages generally involve writeback. Not every file-backed page must be written before eviction.
  • Anonymous memory and swap do not imply physical storage I/O. ZRAM compresses data in RAM, consuming memory and CPU; configured backing-device writeback can also involve storage. Inspect /proc/swaps and device configuration rather than assuming every phone uses RAM-only swap. See the Linux 6.6 ZRAM documentation.
  • A page fault need not read storage. Page/swap-cache hits and first mappings differ from faults requiring I/O. A slow return to an app may also be a cold start after process death, not merely swap-in.

Old names such as do_page_fault, lock_page_or_retry, and wait_on_page_bit_killable are investigation starting points, not a fixed chain on every architecture/version. Linux 6.6 file-cache paths use many folio interfaces. PG_locked and PG_writeback are separate states: folio_unlock() and folio_end_writeback()handle unlocking and writeback completion separately. The original statement that page locks clear only when writeback starts or read I/O finishes was incorrect; killable waits may also be interrupted by fatal signals.

Excessive readahead can create unused I/O and cache pressure; too little can hurt sequential access. Tune to access patterns, hits, and latency rather than assuming smaller is always better.

Case Study: App Startup Under Low Memory

I labeled these two captures low memory and normal memory at the time. Their arrows span bindApplication to around the first frame, not the full launch from the user’s tap or an independently verified presentation timestamp. Thermal, cache, and background-work conditions are incomplete, so they are not a controlled experiment varying memory alone.

Low Memory Startup

The arrow reads 2.044 s; the Thread Timeslices table spans 2053.451 ms, a slightly different interval. Running is 682.137 ms. The two Block I/O subclasses sum to 647.450 ms; all four Uninterruptible subclasses sum to 783.385 ms. The original “about 750ms” described only part of the classification.

Low-memory capture: 2.044s arrow and 2053.451ms thread-state table

The CPU Slices list includes HeapTaskDaemon, several kworker entries, and kswapd0. Grouping by name can combine same-named threads across processes; CPU time summed across cores is not foreground wait time or the interval’s wall duration.

Low-memory CPU Slices list with HeapTaskDaemon, kworker and kswapd0

Normal Memory Startup

The arrow reads 1.223 s; the thread-state selection spans 1227.232 ms. Running is 624.278 ms. All three Uninterruptible subclasses sum to 139.417 ms, of which the two Block I/O subclasses contribute 33.097 ms.

Normal capture: 1.223s arrow and 1227.232ms thread-state table

The thread-state tables compare as follows, in milliseconds. Do not mix their boundaries with the arrow selections:

Metric Low-memory sample Normal sample
Selection extent 2053.451 1227.232
Running 682.137 624.278
Runnable 63.060 43.902
Sleeping 524.869 419.635
Uninterruptible, all subclasses 783.385 139.417
Of those, tagged Block I/O 647.450 33.097

The last row is included in the preceding row, not additional time. Uninterruptible states show the largest increase, but subclasses without a Block I/O tag cannot all be counted as disk I/O. The captures also cannot attribute every difference to memory pressure.

The normal sample still lists HeapTaskDaemon and other workers. kswapd0 is visible in the low-memory ranking and absent from the displayed normal ranking; absence from a cropped ranking is not zero reclaim activity. The unequal interval lengths also prevent comparing total CPU milliseconds as if they were rates.

Visible CPU Slices ranking in the normal sample; omitted threads are not necessarily zero

Optimization Strategies

These directions come from my own experience and ideas shared by fellow engineers. Each problem still needs to be examined on its own. Platform tuning belongs to system/OEM work; ordinary apps should manage their own allocations, caches, and background tasks first. An ongoing notification does not prove high memory consumption or justify unconditional process killing. Retaining fewer processes can increase cold starts, lost work, and failed background functions. Optimize responsiveness and reliability, not the free-RAM number alone.

  1. Watermarks: extra_free_kbytes is a historical Android extension, not a universal kernel interface. The Android 16 adaptation script checks for it and otherwise calculates watermark_scale_factor. Their high-watermark effects differ; values are not interchangeable. Higher reserves can reduce burst-driven direct reclaim while increasing background work and reducing cache capacity.
  2. Storage: UFS 3.0 was a hardware-selection example in 2019, not a software fix or performance guarantee. Measure random I/O, queues, tail latency, and thermal state. A phone is not a desktop where replacing an SSD is a routine remedy.
  3. Readahead: Tune read_ahead_kb using access patterns and cache hits; no universal lower-is-better value applies.
  4. Background I/O: Match the device’s cgroup version and enabled controllers. v1 blkio differs from v2 interfaces such as io.max; excessive throttling can delay work the foreground depends on. See Linux 6.6 cgroup v2.
  5. Proactive reclaim: Experiment during verified idle periods while accounting for compression, writeback, CPU, and later refault costs. Periodically clearing caches is not capacity management.
  6. Exit/restart loops: Distinguish lmkd decisions, AMS count limits, crashes, and launch requests. Fix the actual loop; more kills do not imply better efficiency.
  7. kswapd: Tune around watermarks and allocation bursts, observing indicators such as allocstall*. “Periodically reclaim more to the high watermark” is not a universal scheduler policy.
  8. Swappiness: In Linux 6.6, its 0–200 range expresses relative swap versus filesystem paging I/O cost, not a percentage of RAM at which swapping starts. Do not mechanically set ZRAM devices to zero; measure the workload.
  9. Low-RAM products: Compare foreground tail latency, cold starts, exits, power use, and background-function success under comparable conditions. Do not copy a policy from another device based only on RAM size.
  10. Apps: Fix leaks, bound bitmap/object caches, reduce wasteful allocations and background tasks, and move suitable I/O off the main thread without blocking it for results. From Android 14 / API 34, ordinary apps cannot use killBackgroundProcesses() to clean other packages; see the updated Launcher case study.
  11. Unrecoverable incidents: A user-chosen restart can temporarily restore service; it does not fix the cause. Preserve relevant logs, pressure, and process data where possible and investigate the leak or abnormal load.

There are no universal best values. Validate changes with fixed device/build, workload, cache, and thermal conditions, repeated captures, and distributions rather than one improved screenshot.

References

Current mechanisms and versions are linked next to the relevant claims. The original historical references are retained below, not used as the sole authority for current behavior:

  1. Historical reference 1
  2. Historical reference 2

Article on Zhihu

For discussion, see the original Chinese low-memory case study on Zhihu.

About Me && Blog

Below are personal introductions and related links. Looking forward to exchanging ideas with fellow professionals - when three people walk together, one must be my teacher!

  1. Blogger’s Personal Introduction: Contains personal WeChat and WeChat group links.
  2. Blog Content Navigation: A navigation of personal blog content.
  3. Personally Organized and Collected Excellent Blog Articles - Android Performance Optimization Must-Knows: Welcome self-recommendations and recommendations (private WeChat chat is fine)
  4. Android Performance Optimization Knowledge Planet: The paid community stopped operating in 2026 and no longer accepts new members; this link preserves its history.

One person can walk faster, but a group of people can walk farther

WeChat Scan QR Code

CATALOG
  1. 1. Identifying Low Memory
    1. 1.1. Meminfo Data
    2. 1.2. LMK and kswapd Activity
    3. 1.3. /proc/meminfo
  2. 2. System-wide Lag & Slow Response
  3. 3. Impact of Low Memory on Performance
    1. 3.1. Main Thread I/O Latency
    2. 3.2. CPU Contention
    3. 3.3. Process Death-Restart Cycles
    4. 3.4. Memory Reclamation and I/O Triggers
  4. 4. Case Study: App Startup Under Low Memory
    1. 4.1. Low Memory Startup
    2. 4.2. Normal Memory Startup
  5. 5. Optimization Strategies
  6. 6. References
  7. 7. Article on Zhihu
  8. 8. About Me && Blog