What is a memory leak?
Every app needs memory as a resource to do its work. To make sure each app in Android has enough memory, Android system needs to manage memory allocation efficiently. Android runtime triggers Garbage Collection (GC) when memory runs short. The purpose of GC is to reclaim memory by cleaning up objects that are no longer useful. It achieves it in three steps.
- Traverses all object references in memory from GC roots and marks active objects which has references from GC roots.
- All objects which are not marked (garbages) are wiped from memory.
- Rearrange live objects
In short, everything serving the user should be kept in memory and everything else are wiped out from memory to free up resources.
However, when code are written in a bad manner that unused objects are referenced somehow from reachable objects, GC would mark unused objects as useful object and therefore would not be able to remove them. This is called a memory leak.
Why is memory leak bad?
No objects should stay in memory longer than they should. They occupy valuable resources which could otherwise be used for things that provides real value to the user. For Android specifically, it causes the following problems.
1. Less usable memory would be available when memory leak happens. As a result, Android system will trigger more frequent GC events. GC events are stop-the-world events. It means when GC happens, the rendering of UI and processing of events will stop. Android has a 16ms drawing window. When GC takes long than that, Android starts loosing frames. Generally, 100 to 200ms is the threshold beyond which users will perceive slowness in an application .
In Android, application responsiveness is monitored by the Activity Manager and Window Manager system services. Android will display the ANR dialog for a particular application when it detects one of the following conditions :
- No response to an input event (such as key press or screen touch events) within 5 seconds.
BroadcastReceiverhasn't finished executing within 10 seconds.
I am sure no users would love to see this app not responding popup.
2. When your app has memory leaks, it cannot claim memory from unused objects. As a result, it will ask Android system for more memory. But there is a limit. The system will eventually refuse to allocate more memory for your app. When this happens, app user will get an out-of-memory crash. Of course no one likes crashes. Users might uninstall your app or start giving your app bad reviews.
3. Memory leak issues are mostly hard to find in QA/testing. They are hard to reproduce. And the crash report is usually hard for reasoning because it can happen any time, anywhere when memory allocation is refused by Android system.
How to identify a leak?
Finding leaks requires good understanding on how GC works. It requires diligence in writing code and code review. But in Android, there are some good tools which can help you identify possible leaks or make sure if there is a leak when some piece of code seems suspicious.
1. Leak Canary from Square is a good tool for detecting memory leaks in your app. It creates weak references to activities in your app. (You can also customize it by adding watches to any other objects.) It then checks if the reference is cleared after GC. If not, it dumps heap into a .hprof file and analyze it to confirm if there is a leak. If there is one, it shows a notification and in a separate app, it shows the reference tree of how the leak happens. You can find more about Leak Canary in this article: LeakCanary: Detect all memory leaks. I highly recommend that you install Leak Canary to your developer/testing build. It helps developers and QA to find memory leaks before your app reaches the hands of your users.
2. Android Studio has a handy tool for detecting memory leaks. If you suspect a piece of code in you app might leaks an Activity, you can do this.
Step 1: Compile and run the debug build on a device or emulator connecting to you computer.
Step 2: Go to the suspicious activity, then go back to previous activity which will pop the suspicious activity from the task stack.
Step 3: In Android Studio -> Android Monitor window -> Memory section, click on
Initiate GC button. Then click on
Dump Java Heap button.
Step 4: When
Dump Java Heap button is pressed, Android Studio will open the dumped
.hprof file. In the hprof file viewer, there are a couple of ways you can check the memory leak. You can use the
Analyzer Tasks tool on the top right conner to detect leaked activities automatically. Or you can switch the view mode to
Package Tree View from top left conner, find the activity which should be destroyed. Check the
Total Count of the activity object. If there are 1 or more instances, it means there is a leak.
Step 5: Once you find the leaked Activity, check the reference tree on the bottom and find out what object is referencing the should-have-been-dead activity.
You can find more information about the Android Studio feature from HPROF Viewer and Analyzer.
What are the common leak patterns?
There are lots of ways you can cause a memory leak in Android. To summarize, there are mainly three categories.
- Leak activity to a static reference
- Leak activity to a worker thread
- Leak thread itself
In my Github repo SinsOfMemoryLeaks, I made an app which just leak memories in various ways.
SinsOfMemoryLeaks - Some common patterns of memory leaks in Android development and how to fix/avoid them
Leak branch, you can see all the code with various memory leaks. You can also run it on a device or emulator and use the fore-mentioned tools to track the leaks. In
FIXED branch, you will see the how the leaks are fixed. If you are not convinced, you can again use the fore-mentioned tools to see if the leaks are really fixed. The two branches have different app ids, so you can install them on the same device and play with them side by side.
Now I will quickly go through leaks of different flavors in the 3 main categories.
Leak activity to a static reference
A static reference lives as long as your app is in memory. An activity has lifecycles which are usually destroyed and re-created multiple times during you app’s lifecycle. If you reference an activity directly or indirectly from a static reference, the activity would not be garbage collected after it is destroyed. An activity can range from a few kilo bytes to many mega bytes depending on what contents are in it. If it has a large view hierarchy or high resolution images, it can make a large chunk of memory leaked.
A few flavors of leak in this category can be
- Leak Activity to a static view (https://github.com/frank-tan/SinsOfMemoryLeaks/blob/LEAK/app/src/main/java/com/franktan/memoryleakexamples/viastaticreference/LeakActivityToStaticViewActivity.java)
- Leak Activity to a static variable (https://github.com/frank-tan/SinsOfMemoryLeaks/blob/LEAK/app/src/main/java/com/franktan/memoryleakexamples/viastaticreference/LeakActivityToStaticVariableActivity.java)
- Leak Activity to a singleton object (https://github.com/frank-tan/SinsOfMemoryLeaks/blob/LEAK/app/src/main/java/com/franktan/memoryleakexamples/viastaticreference/LeakActivityToSingletonActivity.java)
- Leak Activity to a static instance of a inner class of the activity (https://github.com/frank-tan/SinsOfMemoryLeaks/blob/LEAK/app/src/main/java/com/franktan/memoryleakexamples/viastaticreference/LeakActivityToStaticInnerClassActivity.java)
Leak activity to a worker thread
A worker thread can also out-live an Activity. If you reference an Activity directly or indirectly from a worker thread which lives longer than the Activity, you also leak the Activity object. A few flavor of this category can be
- Leak Activity to a thread (https://github.com/frank-tan/SinsOfMemoryLeaks/blob/LEAK/app/src/main/java/com/franktan/memoryleakexamples/vialongrunningtask/LeakActivityToThreadActivity.java)
- Leak Activity to a handler (https://github.com/frank-tan/SinsOfMemoryLeaks/blob/LEAK/app/src/main/java/com/franktan/memoryleakexamples/vialongrunningtask/LeakActivityToHandlerActivity.java)
- Leak Activity to an AsyncTask (https://github.com/frank-tan/SinsOfMemoryLeaks/blob/LEAK/app/src/main/java/com/franktan/memoryleakexamples/vialongrunningtask/LeakActivityToAsyncTaskActivity.java)
The same principle applies to other threading techniques such as a
thread pool or
Leak the thread itself
Every time you start a worker thread from an activity, you are responsible of managing the worker thread yourself. Because the worker thread can live longer than the Activity, you should stop the worker thread properly when the Activity is destroyed. If you forget to do so, you are risking leaking the worker thread itself. Example is here https://github.com/frank-tan/SinsOfMemoryLeaks/blob/LEAK/app/src/main/java/com/franktan/memoryleakexamples/vialongrunningtask/LeakThreadsActivity.java
What is the impact of a particular leak?
Ideally you should avoid writing any code which causes memory leaks in the first place, and fix all memory leaks existing in your app. But in reality, if you are dealing with an old code base and need to prioritize tasks including fixing memory leaks, you can evaluate the severity in the following aspects.
1. How big is the leaked memory?
Not all memory leaks are equal. Some leaks a few kilo bytes; some may leak many mega bytes. You can find it out by using the fore-mentioned tools and decide if the size of memory leaked is critical for the devices of your user base.
2. How long does leaked object reside in memory for?
Some leaks through work thread lives as long as the worker thread itself. You should examine how long in the worst scenario your worker thread lives. In my code examples, I have endless loops in the worker thread, so it hold on the leaked object in the memory forever. But in reality, most worker thread does simple tasks such as accessing file system or doing network calls, which is either short lived or you would usually set a timeout anyway. This max time for the leaking is a consideration in determining the priority of fixing memory leaks.
3. How many objects can it leak?
Some memory leak leaks only one object, such as the ones in the static references example in my repo. As soon as the new activity is created, the reference start referencing the new activity. The leaked old activity is clear to be garbage collected. So the max leak is always the size of one activity instance. Other leaks, however, keep leaking new objects as they are created. In the Leaking Threads example, the activity leak one thread every time it is created. So if you rotate the device 20 times, 20 worker threads are leaked. This can be really bad as the app will soon chow up all available memory on the device if it keeps leaking new instances. I would most likely fix all this type of leaks even if one object instance is relatively small.
How to fix/avoid it?
Check out the
FIXED branch https://github.com/frank-tan/SinsOfMemoryLeaks/tree/FIXED of my repo. The key takeaways are
- Be very careful when you decide to have a static variable in your activity class. Is it really necessary? Is it possible that the static variable references activity directly or indirectly (indirectly can be referencing inner class object, an attached view, etc.)? If so, do you clear the reference on Activity onDestroy?
- When you pass your activity as a listener to a singleton object or a x-manager instance, make sure you understand what the other object does with the activity instance you passed in. Clear the reference (set the listener to null) if necessary on Activity onDestroy.
- When you create an inner class in your activity class, make it static if possible. Inner classes and anonymous classes have a implicit reference to the containing class. So if the instance of the inner/anonymous class lives longer than the containing class, you are in trouble. For example, if you create an anonymous runnable class and pass it to a worker thread, or an anonymous handler class and use it to pass tasks to a different thread, you are risking leaking the containing class object. To avoid the leaking risk, use static class as opposed to inner/anonymous class.
- If you are writing a singleton or a x-manager class, you need to store reference of a listener instance, and you don’t have control of how the user of your class manage the reference, you can use
WeakReferencefor the listener reference.
WeakReferencedoes not prevent their referents from being cleared from GC and reclaimed . Although this feature sounds great in preventing memory leaks, it may also be a side effect, because there is no guarantee the referenced object is live when they are needed. So use it as the last resort in fixing memory leaks.
- Always terminate worker threads you initiated on Activity onDestroy().
We studied what a memory leak is, how it happens, what consequence it causes in Android system. Then we introduced two tools of detecting and identifying memory leaks, examined common memory leak patterns in Android, how to evaluate the severity of a leak and how to avoid/fix the common leaks. Don’t forget to check out the code examples for the common memory leak patterns and fixes from my Github repo. Happy making Android apps, everyone :)
SinsOfMemoryLeaks - Some common patterns of memory leaks in Android development and how to fix/avoid them
 Keeping Your App Responsive https://developer.android.com/training/articles/perf-anr.html
 Java Memory Management https://www.dynatrace.com/resources/ebooks/javabook/how-garbage-collection-works/
 HPROF Viewer and Analyzer https://developer.android.com/studio/profile/am-hprof.html
 WeakReference https://developer.android.com/reference/java/lang/ref/WeakReference.html
 Finally understanding how references work in Android and Java https://medium.com/google-developer-experts/finally-understanding-how-references-work-in-android-and-java-26a0d9c92f83#.h9w7hp13h