How to avoid memory leak — Android and JVM languages

Weidian Huang
8 min readSep 13, 2020

--

Although there are a number of articles talking about Android memory leak, I still want to discuss it from another perspective.

What is a memory leak

A Memory Leak happens when there are objects present in the heap that is no longer used, but the garbage collector(GC) is unable to remove them from memory. The reason that the garbage collector cannot remove them is that they are still referenced by other used objects, even they are unused already. Improper referencing may cause a memory leak.

Categories of Android memory leak

The reason for memory leaks in Android can be divided into 2 categories. As long as you can take care of these 2 categories of issues, you can safely avoid memory leaks, so you don’t need to remember 8 or even 10 different reasons.

  1. Longer lifecycle components reference shorter lifecycle components
  2. Resources should be closed, but not closed

1. Longer lifecycle components reference shorter lifecycle components

Android has complicated lifecycles for different components, it confuses a lot of Android beginners. The above diagram shows all the lifecycles of different Android components that you need to know. From top to bottom, the lifecycles are basically shorter and shorter.

While the reference relationship normally is from bottom to top, only the shorter life component needs the direct reference to the longer one. For example, we use getActivity() to get the activity reference from the fragment, or we get the application context from activity and fragment.

If the longer life component holds a reference to the shorter one, memory leak could happen.

A well-known example of this memory leak is the singleton holding a reference to an activity context. Be aware that singleton in the above diagram is at the 2nd level, while activity is at the 4th level.

When we set an activity context to Singleton, Singleton will hold the reference of the activity in its private variable context. The solution to this is to use the application context instead of the activity context. Because singleton and application are at the same level in our diagram and have the same lifecycle, they can hold each other safely.

It is worth noting that if we pass an activity context to setString() method, it does not cause any memory leak, because context here is only a local variable inside the method (it’s inside the stack memory), when the method is returned, the reference will be released. In other word, Singleton doesn’t hold the activity context reference by calling the setString() method.

Similarly, if the application class(the class extended from Android Application()) holds a reference of an activity, or a fragment holds a reference of its views, both will cause a memory leak. Why a fragment cannot hold the reference of its views, you can read my another article below.

If you expect the fragments have the same lifecycle as their activity, you can hold their references directly, otherwise use FragmentManager to help you to manage the lifecycle of the fragments.

A lot of memory leaks are caused by shorter life components holding the reference of a longer one.

Static Variables

In the Java world, static fields have a life that usually matches the entire lifecycle of the running application, unless you set it to null purposely. Try not to hold any static fields inside your application class, activity or fragment, unless you know how it works.

Unregistered Listeners

Another example of a memory leak is an unregistered listener. If a registered listener is not unregistered, a memory leak could happen. But does that mean every listener we used, we have to unregister them? Apparently no. When we use setOnClickListener() for buttons inside an activity, normally we won’t unset the listener. Why? Because buttons have the same lifecycle as the activity, when the activity is destroyed, all the views inside the activity will be destroyed also.

So in which scenario we need to unregister the listener? When the listener object and the target object have a different lifecycle. For example, system services are at the first level in our diagram, if we register a listener to it inside an activity, we have to unregister the listener as below.

For best practice, always unregister a listener whenever possible, so you don’t need to worry if they have the same lifecycle.

Inner nested class and Anonymous class

In Java, any instance of an inner class contains an implicit reference to its outer class, the same as an anonymous class. Because the reference is hidden, we often ignore the issue. Let’s take a look at a common Java memory leak example.

Supposing there is an inner class Leak inside the MemoryLeakFactory, we use MemoryLeakFactory to create Leak only, no other purpose.

But in the above example, after we created Leak instance and store them in mHoles, MemoryLeakFactory should be released and collected by GC, because we don’t need it anymore. But in fact, every Leak the instance holds an implicit reference to its factory, all 2000 MemoryLeakFactory are not be able to be garbage collected.

The problem of the above example is that MemoryLeakFactory and Leak have a different lifecycle. The solution is to use a static nested class for Leak, and there is no implicit reference anymore. And to hold the reference to the outer class, you have to use WeakReference. Because of this reason, Kotlin treats a nested class as a static nested class by default. If you want an inner class in Kotlin, you need to put the inner keyword to the class.

public class MemoryLeakFactory {    // This is static in Kotlin
public class Leak {
}}

But does that mean we totally cannot use an inner class? No, as long as we are sure the inner class has the same lifecycle as the outer class in any case, we are safe to use an inner class.

In Android, there is a common example of memory leak also by using inner class, that is the AsyncTask. Because AsyncTask will create a background thread, which is at the 3rd level in our diagram, it can live longer than our activity or fragment, and memory leak could happen. If we use AsyncTask as an inner class, we should make it a static class.

Handler

If we create an Handler by using an inner class, it will have the same situation. The Runnable object will implicitly reference the Activity it was declared in and will then be posted as a Message on the Handler’s MessageQueue. As long as the message hasn’t been handled before the Activity is destroyed, the chain of references will keep the Activity live in memory and will cause a leak.

System Services

System services are the bridges between system libraries/hardware layer and our application.

Most of the services will hold the reference of its context when getSystemService() is called. Below are 2 different ways of getting the sensor manager.

mSensorManager = (SensorManager)getContext().getSystemService(SENSOR_SERVICE);mSensorManager = (SensorManager)getApplicationContext().getSystemService(SENSOR_SERVICE);

The first one will hold the context from the activity context, the second one will hold the context from the application context. And the first one has a potential memory leak, because if the sensor manager and the activity cannot be destroyed at the same time, a memory leak will happen.

This is an example of a memory leak captured by LeakCanary. The problem is because PowerManager holds the context of MainActivity.

So when should we use the activity context, when should we use the application context? Here are some general rules.

  • If the system service is related to UI, use the activity context. For example, LayoutInflator
  • If the system service is directly or indirectly using the hardware in the device, the application context should be used to avoid memory leaks. For example, LocationManager, FingerprintManager, ConnectivityManager, AlarmManager, CameraManager
  • If LeakCanary detected a memory leak as the above example, try to use the application context.

Circular Reference/Retain Cycle

When object A holds the reference of object B, and object B at the same time hold the reference of object A, in this case, we created a circular reference or retain cycle.

For Java/Kotlin, the garbage collector can collect the memory for both of them if there are no other objects referencing to them. The below article explains how it works.

If we can make sure both object A and object B has the same lifecycle, we don’t need to worry about the memory leak. But for best practice, normally we also need to break the retain cycle between them. To do this, we can set the reference of object A to object B to null. A well-known example is the presenter and view in MVP pattern.

1. Conclusion

When an object needs to hold a reference of another object, be careful of their lifecycles. If their lifecycles are not identical, please make sure the holder object has a shorter lifecycle, otherwise memory leak could happen. For Android, you need to understand the lifecycles of those components in the above diagram, and don’t let the longer life component hold reference of the shorter one directly.

2. Resources should be closed, but not closed

Sometimes, it’s inevitable to use the reference to longer life components. For example, inside the activity or view model, we need to hold the reference of a background thread for API request. In this case, we can limit the background thread to not live longer than the activity or view model.

Kotlin Coroutine

To be able to cancel the Kotlin coroutine, you have to initialize the CoroutineScope with aJob.

Try to use viewModelScope inside a view model, because it has taken care of the lifecycle of view model. When the view model onClear() is called, viewModelScope will be cancel also.

CompositeDisposable

Same as Kotin Coroutine, when the view model is destroyed, dispose() of CompositeDisposable should be called also.

AsyncTask

AsyncTask is not a good design, so it’s not commonly used nowadays, but it could be still used in some old projects. We cannot easily cancel or dispose an AsyncTask. There is a cancel method for AyncTask, but it doesn’t stop the background task immediately, you still need to check if the task is canceled manually by calling isCancelled() inside the doInBackground(), and exit return from doInBackground() directly once the task is canceled.

2. Conclusion

Whenever we make a new connection or open a stream, the JVM allocates memory for these resources, we need to close them when they are not used anymore. A few other examples include database connections, input streams, and session objects.

Conclusion

This article explained the memory leak from a perspective of the Android components lifecycle. Hopefully, after reading, you know why a memory leak could happen and how to avoid it.

Thanks for reading.

If you enjoyed this article, feel free to hit that clap button 👏 to help others find it.

--

--