Memory allocation and de-allocation in Android always comes at a cost. The Chinese saying “Easy to be luxurious from frugal, hard to be frugal from luxurious” is an appropriate turn of phrase for memory usage.
Let’s imagine a worst-case scenario of compiling an application with millions of lines of code, when suddenly an out of memory (OOM) crash is triggered. You start debugging the application and analyzing the hprof file. Luckily, you locate the root cause and fix the memory killer. But sometimes you are unlucky and find that so many tiny member variables and temporaries are allocating memory that there is no simple fix, meaning you have to refactor code, which involves potential risk, just to save kilobytes or even bytes of memory.
This article introduces Android Memory Management and explains various aspects that play a role in the management system. Additionally, improving memory management, detecting and avoiding memory leaks, and analyzing memory usage are covered.
Android Memory Management
Android uses paging and mmap instead of providing swap space, which means any memory your application touches cannot be paged out unless you release all references.
The Dalvik* Virtual Machine’s heap size for application processes is limited. Applications start up with 2 MB, and the maximum allocation, marked as "largeHeap," is limited to 36 MB (depending on the specific device configuration). Examples of large heap applications are Photo/Video Editor, Camera, Gallery, and Home Screen.
Android stores background application processes in a LRU cache. When the system runs low on memory, it will kill processes according to the LRU strategy, but it will also consider which application is the largest memory consumer. Currently the maximum background process count is 20 (depending on the specific device configuration). If you need your app to live longer in the background, de-allocate unnecessary memory before moving to the background and the Android system will less likely generates error message or even terminates the app.
How to Improve Memory Usage
Android is a worldwide mobile platform and millions of Android developers are dedicated to building stable and scalable applications. Here is a list of tips and best practices for improving memory usage in Android applications:
- Be careful about using a design pattern with "abstraction". Although from the point of view of design pattern, abstraction can help to build more flexible software architect. In mobile world, abstraction may involve side effect for its extra code to be executed, which will cost more time and memory. Unless abstraction can provide your application a significant benefit, you would be better not to use it.
- Avoid using "enum". Enum will double memory allocation than ordinary static constant, so do not use it.
- Try to use the optimized SparseArray, SparseBooleanArray, and LongSparseArray containers instead of HashMap. HashMap allocates an entry object during every mapping which is a memory inefficient action, also the low performance behavior - “autoboxing/unboxing” is spread all over the usage. Instead, SparseArray-like containers map keys into plain array. But please remember that these optimized containers are not suitable for large numbers of items, when executing add/remove/search actions, they are slower than Hashmap if your data set is over thousands of records.
- Avoid creating unnecessary objects. Do not allocate memory especially for short-term temporary objects if you can avoid it, and garbage collection will occur less when fewer objects are created.
- Check the available heap of your application. Invoke ActivityManager::getMemoryClass() to query how many heap(MB) is available for your application. OutofMemory Exception will occur if you try to allocate more memory than is available. If your application declares a "largeHeap" in AndroidManifest.xml, you can invoke ActivityManager::getLargeMemoryClass() to query an estimated large heap size.
- Coordinate with the system by implementing onTrimMemory() callback. Implement ComponentCallbacks2::onTrimMemory(int) in your Activity/Service/ContentProvider to gradually release memory according to latest system constraints. The onTrimMemory(int) helps overall system response speed, but alsokeep your process alive longer in the system.
When TRIM_MEMORY_UI_HIDDEN occurs, it means all the UI in your application has been hidden and you need to free UI resources. When your application is foreground, you may receive TRIM_MEMORY_RUNNING[MODERATE/LOW/CRITICAL], or in the background you may receive TRIM_MEMORY_[BACKGROUND/MODERATE/COMPLETE]. You can free non-critical resources based on the strategy to release memory when system memory is tight.
- Services should be used with caution. If you need a service to run a job in the background, avoid keeping it running unless it's actively performing a task. Try to shorten its lifespan by using an IntentService, which will finish itself when it's done handling the intent. Services should be used with the caution to never leave one running when it's not needed. Worst case, the overall system performance will be poor and users will find your app and uninstall it (if possible).
But if you are building an app that needs to run for a long period of time, e.g., a music player service, you should split it into two processes: one for the UI and the other for the background service by setting the property "android:process" for your Service in AndroidManifest.xml. The resources in the UI process can be released after hidden, while the background playback service is still running. Keep in mind that the background service process MUST NOT touch any UI; otherwise, the memory allocation will be doubled or tripled!
- External libraries should be used carefully. External libraries are often written for non-mobile device and can be inefficient in Android. You must take into account the effort in porting and optimizing the library for mobile before you decide to use it. If you are using a library for only one or two features from its thousands of other uses, it may be best to implement it by yourself.
- Use bitmaps with correct resolution. Load a bitmap at the resolution you need, or scale it down if the original bitmap is a higher resolution.
- Use Proguard* and zipalign. The Proguard tool removes unused code and obfuscates classes, methods and fields. It will compact your code to reduce required RAM pages to be mapped. The zipalign tool will re-align your APK. More memory will be needed if zipalign not running because resource files cannot mapped from the APK.
How to Avoid Memory Leaks
Use memory carefully with above tips can bring benefit for your application incrementally, and make your application stay longer in system. But all benefit will lost if memory leakage happens. Here are some familiar potential leakage that developer needs to keep in mind.
- Remember to close the cursor after querying the database. If you want to keep the cursor open long-term, you must use it carefully and close it as soon as the database task finished.
- Remember to call unregisterReceiver() after calling registerReceiver().
- Avoid Context leakage. If you declare a static member variable "Drawable" in your Activity, and then call view.setBackground(drawable) in onCreate(), after screen rotate, a new Activity instance will be created and the old Activity instance can never be de-allocated because drawable has set the view as callback and view has a reference to Activity (Context). A leaked Activity instance means a significant amount of memory, which will cause OOM easily.
There are two ways to avoid this kind of leakage:
- Do not keep long-lived references to a context-activity. A reference to an activity should have the same life cycle as the activity itself.
- Try using the context-application instead of a context-activity.
- Avoid non-static inner classes in an Activity. In Java, non-static anonymous classes hold an implicit reference to their enclosing class. If you're not careful, storing this reference can result in the Activity being retained when it would otherwise be eligible for garbage collection. So instead, use a static inner class and make a weak reference to the activity inside.
- Be careful about using Threads. Threads in Java are garbage collection roots; that is, the Dalvik Virtual Machine (DVM) keeps hard references to all active threads in the runtime system, and as a result, threads that are left running will never be eligible for garbage collection. Java threads will persist until either they are explicitly closed or the entire process is killed by the Android system. Instead, the Android application framework provides many classes designed to make background threading easier for developers:
- Use Loader instead of a thread for performing short-lived asynchronous background queries in conjunction with the Activity lifecycle.
- Use Service and report the results back to the Activity using a BroadcastReceiver.
- Use AsyncTask for short-lived operations.
How to Analyze Memory Usage
To know more about memory usage statistics in online/offline, you can check the Android main log by using logcat command in Android Debug Bridge (ADB), dump memory info for specific package name, or using other tools like Dalvik Debug Monitor Server (DDMS) and Memory Analyzer Tool (MAT), here are some brief introductions about the ways to analyze the memory usage of your application.
- Understand garbage collection (GC) log messages for the Dalvik Virtual Machine, as shown in the following example and definition:
GC Reason: What triggered the garbage collection and what kind of collection is it? Reasons may include:
- GC_CONCURRENT: A concurrent garbage collection that frees up memory as your heap begins to fill up.
- GC_FOR_ALLOC: A garbage collection occurs because your app attempted to allocate memory when your heap was already full, so the system had to stop your app and reclaim memory.
- GC_HPROF_DUMP_HEAP: A garbage collection that occurs when you create an HPROF file to analyze your heap.
- GC_EXPLICIT: An explicit garbage collection, such as when you call gc() (which you should avoid calling and instead trust the garbage collector to run when needed).
- Amount freed: The amount of memory reclaimed from this garbage collection.
- Heap stats: Percentage free and (number of live objects) / (total heap size).
- External memory stats: Externally allocated memory on API level 10 and lower (amount of allocated memory) / (limit at which collection will occur).
- Pause time: Larger heaps will have larger pause times. Concurrent pause times show two pauses: one at the beginning of the collection and another near the end.
- The larger the GC log, the more memory allocation/de-allocation occurred in your application, which also means the user experience is choked.
- View overall memory allocations.
By executing the adb command: “adb shell dumpsys meminfo
”, you can see all of your application's current allocations, measured in kilobytes.
Generally, you should be concerned with only the "Pss Total" and "Private Dirty" columns. The "Pss Total" includes all Zygote allocations (weighted by their sharing across processes, as described in the PSS definition above). The "Private Dirty" number is the actual RAM committed to your application’s heap, your own allocations, and any Zygote allocation pages that have been modified since forking your app’s process from Zygote.
Additionally, the "ViewRootImpl" shows the number of root views that are active in your process. Each root view is associated with a window, so this can help you identify memory leaks involving dialogs or other windows. The "AppContexts" and "Activities" show the number of application Context and Activity objects that currently live in your process. This can be useful to quickly identify leaked Activity objects that can’t be garbage collected due to static references on them, which is common. These objects often have a lot of other allocations associated with them and are a good way to track large memory leaks.
- Capture a heap dump and analyze it with the Eclipse* Memory Analyzer Tool (MAT).
You can directly capture a heap dump by using DDMS or calling Debug::dumpHprofData() in your source code for more precise results. Then you need to use the hprof-conv tool to generate the converted HPROF file. The following screenshot is the memory analyze result showing in the MAT.
To build more memory friendly applications, Android developers need to have a basic understanding of Android memory management. Developers should practice efficient memory usage, use analysis tools, and implement the tips provided in this paper. It is better to build a stable and scalable application first, rather than applying fixes during the implementation period.