Posted by Alice Yuan, Developer Relations Engineer, Ajesh Pai, Developer Relations Engineer, and Fung Lam, Developer Relations Engineer
While app performance is often equated with a smooth UI and fast start times, memory serves as the silent foundation upon which these visible metrics are built. It’s no secret that we’re seeing a shift where device memory is more important than ever. Not only have we made strides in Android memory optimizations with Android 17, we’re providing the tooling and API support to help you stay ahead of stricter memory requirements later this year.
To ensure device stability, starting in Android 17, the system will begin enforcing app memory limits based on the device’s total RAM. If an app exceeds those limits, Android will kill the process with no associated stack trace.
Beyond these forced terminations, unoptimized memory usage inevitably degrades the user experience. When the app approaches heap memory limits, it triggers frequent garbage collection—leading to noticeable UI stutters. Furthermore, when a device runs out of available memory, the system scrambles to reclaim pages, causing CPU strain, UI latency, and battery drain. If the memory shortage is too severe, it can cause Low Memory Killer (LMK) events that abruptly terminate background processes and force apps to have slow cold starts and lose user state.
To build highly performant apps and avoid these forced terminations, we recommend that you adopt the following memory optimization strategies:
A condensed version of this blog post is also available in video format, go check it out!
Understanding Android 17 app memory limits
App memory limits are being introduced in Android 17 to prevent “one bad actor” from destroying the multitasking experience and stability of the user’s entire device.
Here is a breakdown of the reasons driving this architectural change:
To determine if your app session was impacted by these constraints in the field, you can call getDescription() within ApplicationExitInfo. If the system applied a limit, the exit reason is reported as REASON_OTHER and the description string will contain “MemoryLimiter:AnonSwap”. You can also leverage trigger-based profiling using TRIGGER_TYPE_ANOMALY to automatically capture heap dumps when the memory limit is reached. Furthermore, Android is actively working to surface more in-field memory metrics to developers within the Google Play Console.

We have also expanded our memory limits documentation to include local debugging commands, allowing you to simulate memory constraints in your local environment and validate your application’s behavior under any memory limit enforcement.
Maximize bytecode optimization with R8
A highly effective way to reduce your app’s memory footprint is to enable the R8 optimizer. By shrinking classes, methods, and fields into shorter names and stripping out unused code and resources, R8 significantly reduces your app’s memory footprint by minimizing the amount of resident code required during execution.
R8 minimizes resident code, shrinking the memory footprint and lowering LMK termination risk. This results in more frequent warm starts over slow cold starts. Additionally, streamlined bytecode reduces main-thread CPU overhead, directly cutting ANR rates for a more fluid user experience. For example, the digital bank Monzo enabled full R8 optimization and saw a 35% reduction in their ANR rate, a 30% improvement in cold start rate, and a 9% reduction in overall app size.
The digital bank Monzo enabled full R8 optimization and boosted performance metrics by up to 35%.
To properly configure R8 in your build.gradle file:
If you are using reflection in your code base, then add Keep rules to prevent R8 from optimizing those parts of the code. Make sure to scope the keep rules narrowly to get the maximum optimization.
To get the maximum optimization, make sure to follow these best practices in your keep rule file.
To see more best practices, view our keep rules documentation.
Library Developer R8 Best Practices
If you are a library developer, strictly place the rules your consumers need into your consumer-rules file, and keep your library’s internal protection rules in your proguard-rules.pro file. For more information on how to optimize libraries, see Optimization for library authors.
R8 Configuration Analyzer
To audit your R8 optimization, use the Configuration Analyzer. Configuration analyzer shows the current state of optimization with Obfuscation, Optimization, and Shrinking scores. With configuration analyzer, you can also understand how many classes, methods or fields are prevented from optimization by each keep rule. Refine these broad package wide keep rules to unlock the maximum optimization.

Using configuration analyzer, you can also identify keep rules that are subsuming other keep rules, redundant keep rules and unused keep rules.
The Configuration Analyzer shows the current state of optimization with Obfuscation, Optimization, and Shrinking scores.
R8 Agent Skill
You can also leverage the R8 Agent Skill with Android Studio agent or other AI tools to resolve misconfigurations and refine your rules resulting in improved app performance. (Insights from AI-driven skills will require technical verification)
Optimize image loading
Bitmaps are usually the largest common objects residing in your app’s memory. They represent the final stage of the image loading process where compressed files, like JPEGs or PNGs, are decoded into raw pixel data for display. This means a tiny 100KB compressed image can balloon into several megabytes of RAM because memory consumption is determined by the image’s pixel dimensions and color depth. Since bitmap operations are frequently on the critical path to drawing frames, unoptimized images cause severe memory bloat and UI jank.
Google recommends leveraging image loading libraries Coil for Kotlin-first projects, particularly when developing with Jetpack Compose and Glide for Java-based applications.
Adopt these five best practices
Check out our documentation on Optimizing performance for images to learn more.
Android Studio tooling
You can also eliminate redundant bitmaps using Android Studio Narwhal 4. Here is how to hunt them down in five simple steps:
Look for the yellow warning triangle âš ï¸ in heap dumps when using the Android Studio Profiler.
Detect and fix memory leaks with Android Studio
Memory leaks in Android occur when your code holds onto an object’s reference long after its lifecycle has ended. This prevents the Garbage Collector (GC) from reclaiming that memory, eventually leading to sluggish performance or OutOfMemoryError (OOM).

Android Studio Panda 3 features a dedicated LeakCanary profiler task, allowing developers to analyze real-time memory leaks and map traces within the IDE.
The LeakCanary profiler task in Android Studio actively moves the memory leak analysis from your device to your development machine, resulting in a significant performance boost during the leak analysis phase as compared to on-device leak analysis.
LeakCanary memory leak analysis contextualized with Go to declaration for debugging
Additionally, the leak analysis is now contextualized within the IDE and fully integrated with your source code, providing features like go to declaration and other helpful code connections that drastically reduce the friction and time required to investigate and fix memory leaks.
Examples of common memory leaks
Memory leaks occur when an object persists in memory beyond its intended lifespan. This typically happens due to:
Here are a few example scenarios:
|
Scenario |
Compose-based example |
View-based example |
|
Leaking Context ![]() |
Example: Fix: |
Example: Fix: |
|
Leaking Listeners |
Example: Fix: |
Example: Fix: |
|
Leaking Views |
Example: ![]() Fix: |
Example: Fix: |
Trim memory when app leaves visible state
Android can reclaim memory from your app or stop your app entirely if necessary to free up memory for critical tasks, as explained in Overview of memory management. Android will usually reclaim memory from your app when it’s not visible to the user, such as by discarding some of your app’s code and data pages in memory or compressing your heap allocations. When the user resumes your app and your app tries to access some memory that’s been reclaimed, the OS will swap that memory back in on demand. This swapping behavior can be slow, and cause unexpected jank or stutters in your app.
If you leave it to the OS to decide what memory to reclaim from your app, you may find that the OS reclaimed memory that you’ll need shortly after resuming your app. Instead, your app can voluntarily discard memory allocations that it can regenerate later, on demand and at a low cost. To do so, you can implement the ComponentCallbacks2 interface. You can implement onTrimMemory in your Activity, Fragment, Service, or even your custom Application class. Using it in the Application class is highly effective for global cache management.
The provided onTrimMemory() callback method notifies your app of lifecycle or memory-related events that present a good opportunity for your app to voluntarily reduce its memory usage.
In terms of memory lifecycle management, your implementation should focus exclusively on TRIM_MEMORY_UI_HIDDEN and TRIM_MEMORY_BACKGROUND. Since Android 14, the system has ceased delivering notifications for other legacy constants, which were formally deprecated in Android 15.
TRIM_MEMORY_UI_HIDDEN: This signal indicates that your application’s UI has transitioned out of the user’s view. This provides an opportunity to release substantial memory allocations tied strictly to the interface—such as Bitmaps, video playback buffers, or complex animation resources.
TRIM_MEMORY_BACKGROUND: At this level, your process is residing in the background and is now a candidate for termination to satisfy the system’s global memory needs. To extend the duration your process remains in the cached state, and reduce the number of app cold starts, you should aggressively release any resources that can be easily reconstructed once the user resumes their session.
import android.content.ComponentCallbacks2
// Other import statements.
class MainActivity : AppCompatActivity(), ComponentCallbacks2
/**
* Release memory when the UI becomes hidden or when system resources become low.
* @param level the memory-related event that is raised.
*/
override fun onTrimMemory(level: Int)
if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN)
// Release memory related to UI elements, such as bitmap caches.
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND)
// Release memory related to background processing, such as by
// closing a database connection.
Note: The onTrimMemory integration may depend on SDK support. For instance, certain games rely on their game engine to enable this capability. Please check out the game memory optimization documents.
Advanced memory observability with ProfilingManager
To catch and diagnose memory issues in the field that cannot be reproduced locally, you should leverage the ProfilingManager API. Introduced in Android 15, this advanced observability API allows you to programmatically collect real-user Perfetto profiles.

For teams that lack a dedicated infrastructure to manage and host performance artifacts, Crashlytics is exploring a specialized solution to streamline this workflow. They are inviting developers to provide feedback.
Android 17 introduces new event-driven triggers, most notably TRIGGER_TYPE_OOM and TRIGGER_TYPE_ANOMALY:
val profilingManager =
applicationContext.getSystemService(ProfilingManager::class.java)
val triggers = ArrayList()
triggers.add(ProfilingTrigger.Builder(
ProfilingTrigger.TRIGGER_TYPE_ANOMALY))
val mainExecutor: Executor = Executors.newSingleThreadExecutor()
val resultCallback = Consumer { profilingResult ->
if (profilingResult.errorCode != ProfilingResult.ERROR_NONE)
// upload profile result to server for further analysis
setupProfileUploadWorker(profilingResult.resultFilePath)
profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback)
profilingManager.addProfilingTriggers(triggers)
Once you’ve collected the heap dump, you can download the profile from the server, or locally via adb pull and drag and drop the file into the Perfetto UI. To streamline your memory debugging workflow, use the Heap Dump Explorer, this is the new default view for heap dumps in Perfetto UI. This tool provides an intuitive interface for inspecting Java heap dumps, allowing you to visualize object allocation hierarchies, compute retained memory sizes, and identify the shortest path from garbage collection root. By leveraging the Heap Dump Explorer, you can rapidly pinpoint memory leaks, bloated retained objects such as excessive bitmap allocations, and analyze heap object allocations all in one place.
Use the Heap Dump Explorer’s embedded flamegraph to visually inspect and navigate through objects with the highest heap allocations.
Conclusion
Optimizing bytecode with R8, adopting image loading best practices, and resolving memory leaks are critical steps toward delivering a high-quality user experience while managing resources effectively under pressure. Adopting these proactive measures helps maintain app stability and performance, preventing unexpected terminations while safeguarding user context. To further your performance expertise, explore our revised memory guidance.




