An introduction to memory management and memory leaks on Android

Connie Reinholdsson
The Startup
Published in
10 min readMar 3, 2020

This article gives an overview of memory management and memory leaks on Android. It takes you through a breakdown of memory on Android, as well as explaining how memory leaks occur and how to detect leaks using the Memory Profiler and the LeakCanary library. Finally, it addresses how you can avoid leaks in the future.

Memory Management on Android

Let’s start with looking at the breakdown of memory on Android. Memory on Android is made up of three types:

Random Access Memory (RAM)

RAM is used to temporarily store information whilst running an application. This includes both information from the app currently running and apps running in the background. As it is temporary, the information will be lost when the application is killed or when the device is restarted.

An Android device typically has 4GB of RAM these days, but some more recent devices have 6GB or more. More RAM means more temporary memory, which enables users to do more multi-tasking — however for most people 4GB is plenty of RAM to carry out desired tasks and by default an app is only able to access a fraction of the total RAM.

zRAM

zRAM is a part of RAM and works through compressing unused resources and moving them to a reserved area to free up space which increases the memory available. It’s worth bearing in mind though that the process of compressing and decompressing memory requires the CPU work harder and can slow down device operations.

Storage

Storage holds all persistent data, including photos, videos, music and documents, which will remain if the phone is rebooted. Examples of persistent storage on Android include key / value pairs stored in shared preferences and saving information a database, for example using SQLite, Realm or Room.

Storage on Android devices can vary but these days storage is typically 32GB or 64GB. On a number of devices, storage can be expanded through external storage such as a microSD card.

The process of managing memory is done by The Android Runtime (ART) and its predecessor Dalvik virtual machine. The runtime uses two processes called paging and memory mapping to manage memory. For more detail on these processes, see the links above.

1. What are memory leaks?

So now that we have an understanding of memory breakdown on Android — what are memory leaks and how do they occur? To understand how a memory leak occurs — we need to understand the two types of memory allocation: stacks and heaps.

Stacks and Heaps

  • Stack — Stack is used for static memory allocation. This means the stack objects are temporary and once the function is complete, each object will be released from memory.
  • Heap — Heap is used for dynamic memory allocation. This works differently to stacks in that once the function is complete, the objects will not be reclaimed or released from memory.

To release the objects from the heap, Android uses something called a Garbage Collector (GC) which reclaims unused objects to free up memory. It simply tracks objects in the heap, determines when it’s no longer being used and at that point it releases the objects from the heap. Sounds like that does the job, so what’s the problem? Well, if we accidentally keep hold of a reference to an unused object in the heap, the Garbage Collector thinks it’s still being used and doesn’t recognise it as garbage — meaning it cannot release the object from memory.

If we keep storing more unused objects in the heap and the Garbage Collector cannot reclaim the memory, the objects will keep building up until we eventually run out of space. This could cause our app to stagger, freeze the UI and eventually result in an OutOfMemory exception which crashes the app. 😐

That sounds pretty bad, but how does this actually happen in code? To demonstrate this, I have a simple TaskManager app into which I will introduce a memory leak. I will do by passing in Context into the a Singleton class, a fairly common memory leak pattern.

First, I create a singleton class called SingletonExample which takes context in its constructor.

Then I will create an instance of my SingletonExample class in onCreate() in TaskActivity.

This means the SingletonExample class will hold a reference to the context and therefore the TaskActivity, meaning the GC cannot reclaim the first instance of the TaskActivity as it thinks it’s still being used.

2. Detecting memory leaks

So in this case we know we have a memory leak in our app, but how can we find out if our app leaks memory without manually checking all our code for potential leaks? Luckily, there are a number of tools that can help us with this.

The Memory Profiler

Android Studio provides a tool called the Memory Profiler for detecting memory leaks. The tool breaks down memory into segments, including Java code, Graphics, Stack and number of other types of memory to give an overview of what’s using up memory as you navigate through the application.

Sample app TaskManager showing memory allocations

The Memory Profiler is great to use if you suspect a memory leak on a particular screen, as it allows you to inspect the allocation, deallocation and references to the objects in the heap. However, if you don’t suspect it anywhere in particular, you can try to stress-test your app’s garbage collection to see if any memory leaks are being missed. Use techniques to use up as much memory as possible, for example switch between tabs and rotate the device. That way it’s easier to figure out on which screens potential leaks live.

Memory leaks can vary in terms of complexity and impact, so to begin what you should typically look out for are big objects like Context or Bitmap leaking or leaking objects within a loop which fire frequently. Luckily, we have a suspicion about where it could be — so let’s find it!

View memory allocations

The Memory Profiler allows us to view live allocations whilst interacting with our app. In our case, we suspect TaskActivity is leaking, so by creating a couple of new tasks in the app, we’re instantiating new instances of TaskActivity and then exit the screen to destroy the instances. In the Memory Profiler, we then select that period on the timeline during which the objects have been instantiated (allocated), destroyed and the GC has done it’s magic (deallocated it).

We then filter to findTaskActivity and inspect its allocations and deallocations. Looks like it’s been allocated 3 times but deallocated 2 times! That sounds like a lot like a leak …

Sample app TaskManager showing our leak

To inspect this further, we can open Instance View to track down the suspected instant (in our case the first one) and use the Call Stack window to see where the object was allocated. From this information, we can confirm TaskActivity is leaking upon instantiation— that’s a good start! 😃

Capture heap dumps

To further locate the leak, we can use another useful tool in Memory Profiler which allows us to capture a heap dump. This allows us to inspect the objects and its references in the heap during a specified period.

In our case, we still want to filter on TaskActivity as we suspect it’s leaking and look at its references. What we can see here is that the first instance is being referenced fromSingletonExample class and it’s pointing at mContext, however in the other instances it's not. This further suggests that TaskActivity is leaking when the singleton is created and it’s told us the source — bingo! 😃

TaskManager sample app showing a heap dump

It’s worth bearing in mind that whilst this tool can be very useful for finding references to a suspected leak, it can be quite difficult and complex to use for finding unknown leaks and requires a fair bit of analysis. Saying that, if you do master it — it’s a great tool to have under your belt!

LeakCanary

Image from LeakCanary

Another way to find memory leaks is through using Leak Canary, a library created by Square which helps you detect memory leaks. It works through using an ObjectWatcher class which holds weak references to destroyed objects in the heap. If the reference isn’t cleared in the following five seconds and the Garbage Collector has run, the object is considered retained and logs this as a potential memory leak in the logcat. Pretty clever, huh?

To get started, simply add the library inside your dependencies to your build.gradle file, sync and compile.

Add the LeakCanary library

As soon as you start the application, you can see the LeakCanary output in the logcat as it watches instants and retains objects. To trigger a leak, I added a couple of new tasks in my TaskManager app. Not long after, I got this notification from LeakCanary which indicates a potential leak!

Simply click on the notification to “dump heap” to investigate the potential leak and this dialog will be displayed in the app.

LeakCanary is investigating the leaks ..

Leak Canary then analyses the heap through locating the retained objects and finds the leak trace which is a path of references to each retained object. Once done, a notification will be displayed summarising the number of retained objects and leaks.

Looks like it found our leak! If we click on it we’ll see the full leak trace:

It’s telling us that TaskActivity is leaking and the underlined references show the trace, it’s leaking from mContext in SingletonExample — sounds familiar! Now we know how to find a leak using LeakCanary. 😃

3. Avoiding memory leaks

So now that we’ve found our memory leak, how can we avoid them in the future? Below are some of the most common causes and patterns.

Photo by Bogomil Mihaylov on Unsplash

Broadcast receivers

Broadcast receivers can be used to listen to system-wide broadcast events or intents which indicate device information such as low battery, date and connectivity changes — for example that airplane mode has been switched off. When using them, we need to remember to unregister broadcast receivers, otherwise we will inevitably keep hold of a reference to the activity.

How to avoid it: All you need to do is to call unregister() on your broadcast receiver in onStop() in your activity.

This pattern is also found for asyncTask, TimerTask and threads which need to be cancelled in onDestroy() to avoid a leak.

Context to Singleton class

Sometimes we need to pass in context from an activity to a Singleton class. An example of this would be a utils class where we need to access resources, services or internal files. However, passing in context means we inevitably hold on to a reference to the activity.

How to avoid it: Instead of passing in this from an activity, we can pass in application context if it’s available (if you want to learn more about when to use which context, I found this article really helpful!). An alternative solution is to ensure the we set the Singleton context to null inside the activity onDestroy() method.

Static references

Referencing a view or an activity as static means the reference to the activity will not be garbage collected. This should simply be avoided at all times.

How to avoid it: If you for some reason have to do it, you can ensure it’s destroyed by setting it to null in onDestroy().

Inner class references

Inner classes often cause leaks by holding an implicit reference to the outer class. This happens the class variable is declared as static or if the class itself is not declared as static. Confusing? Yes, but it’s an easy one to avoid if we follow the simple rule below.

How to avoid it: Make the inner class static to avoid holding a reference to the outer class and never create a static variable of an inner class. The same applies to anonymous classes.

That’s it! Hope you’ve learnt some stuff about memory leaks and how to avoid them. Happy coding! 😄

Here are a number of articles and documentation I found particularly useful when learning about memory leaks:

--

--