In this article, we’ll tell you what a memory leak is, how it happens and what are the consequences for the Android operating system. Also consider tools for detecting memory leaks, typical models of memory leaks in Android, ways of assessing the degree of criticality and methods of preventing major types of leaks.
Each application needs a working memory for normal operation. To provide the required amount of memory for all applications, Android must effectively manage the allocation of memory for each process. The Android runtime starts garbage collection (GC) when the RAM is running low.
What is a garbage collector?
Java Memory Management with built-in garbage collector is one of the best achievements of this language. It allows developers to create new objects without worrying about allocating memory and releasing it, since the garbage collector automatically restores memory for reuse. This provides faster development with less code, while eliminating memory leaks and other problems associated with it. At least in theory.
Ironically, the Java garbage collector works too well, creating and deleting a large number of objects. Most memory management problems are solved, but often due to a decrease in performance. The creation of a universal garbage collector, applied to all possible situations, led to difficulties with the optimization of the system. To deal with the garbage collector, you first need to understand how memory management works in the Java Virtual Machine (JVM).
How does the garbage collector work?
Many people think that the garbage collector collects and deletes unused objects from memory. In fact, the Java garbage collector does everything the other way around. Live objects are marked as active, and everything else is considered garbage. As a consequence, this fundamental feature can lead to many performance problems.
Let’s start with the so-called heap (the heap), the area of memory used to dynamically allocate application resources. In most configurations, the operating system pre-empts this part under JVM control while the program is running. This leads to the consequences:
- the creation of an object is faster because global synchronization with the operating system is not required for each individual object. In the process of allocating memory under the JVM application, it simply fixes a certain area of memory for the task and moves the displacement pointer forward (picture below). The next distribution begins with this offset and occupies the next memory location;
- When an object is no longer used, the garbage collector restores the basic state of this memory location and reuses it to accommodate another object. This means that there is no explicit deletion and the memory will still not be cleared.
All objects are placed in a heap controlled by the JVM. Each element used by the developer is handled in this way, including class objects, static variables, and even the code itself. While the object refers to something, the JVM considers it to be used. When the object no longer references and, therefore, is unavailable by the application code, the garbage collector deletes it and restores the unused memory. Everything is as simple as it sounds, but the question is: what is the first link in the object tree?
The roots of the garbage collector are the initial position of all hierarchies (trees) of objects
Each object tree must have one or more root objects. While the application can reach these roots, the whole tree is available. But when these root objects are considered available? Special objects, called the roots of the garbage collector (the roots of GC, the figure below) are always available, as well as any object whose root is the root of the garbage collector.
In Java, there are the following types of root of the garbage collector:
- Local variables are supported by the active flow stack. This is a dummy virtual reference to the object and, therefore, it is not visible. For all purposes and tasks, local variables are the roots of the garbage collector;
- active Java threads are always considered to be used objects and therefore are the roots of a garbage collector. This is especially important for local flow variables;
- static classes are referred to by static variables. This makes them de facto roots of the garbage collector. The classes themselves can be collected by the collector, which will remove all the static variables they refer to. This is especially important when we use application servers, OSGi containers or class loaders in general.
The roots of the garbage collector are objects that reference the JVM and thus remain in the device’s memory.
Therefore, a simple Java application has the following roots of a garbage collector:
- local variables in the main method;
- main stream;
- static variables of the main class.
Marking and garbage collection
To determine which objects are no longer used, the JVM periodically starts the algorithm for marking and garbage collection:
- The algorithm “runs through” the entire hierarchy of objects, starting with the roots of the garbage collector, and marks each found object as active.
- All parts of memory that do not contain active objects (or rather objects that were not marked in the previous step) are restored. They are simply designated as free.
The garbage collector is designed to eliminate the cause of memory leak-unattainable but not deleted objects in memory. However, this only works for memory leaks in the classical sense. It’s possible that unused objects are still available to the application, because the developer simply forgot to clear links to them. Such objects can not be collected by the collector. Worse, such a logical memory leak can not be detected by any software.
When objects no longer refer directly or indirectly to the root of the garbage collector, they will be deleted. As you can see, the built-in garbage collector handles well with classic memory leaks. Other types of memory leaks can be handled by other software, which will be discussed later.
In simple words, only those objects that are used by the user remain in memory.
However, when the code is written poorly, unused objects can reference non-existent objects, and the garbage collector marks them as active and can not delete them. This is called a memory leak.
Why is a memory leak – is it bad?
No object should remain in memory for longer than necessary. After all, these resources can be useful for tasks that can have real value for the user. In particular, for Android this causes the following problems:
First, when leaks occur, the memory available for memory usage becomes smaller, which causes more frequent starts of the garbage collector. Such launches stop the rendering of the user interface, as well as stop other components necessary for the normal operation of the system. In such cases, the drawing of the frame lasts longer than the usual 16 ms. When the drawing drops below 100 ms, users will begin to notice slowdowns in the applications.
In Android, the responsiveness of applications is controlled by the activity manager and the window manager. The system will open the ANR dialog (the application does not respond) for a particular application when one of the following conditions is met:
- The application does not respond to keystrokes, or presses the screen for 5 seconds;
BroadcastReceivernot completed within 10 seconds;
It’s unlikely that users will like to see this message on the screens of their gadget.
Second, an application with a memory leak can not get additional resources from unused objects. It will make a request to allocate additional memory, but there is a limit to everything. Android will refuse to allocate more memory for such applications. When this happens, the application will simply fall. This can cause negative emotions for users, and they, in turn, can not only delete the application, but also leave negative feedback about it in the application store.
How to identify a leak?
To determine the memory leak, it is necessary to understand the work of the garbage collector very well. But Android can also provide some good tools that can help identify possible leaks or find a suspicious piece of code.
The Leak Canary app from Square is a good tool for detecting memory leaks in an application. It creates links to objects in your application and checks to see if these links are removed by the garbage collector. If not, then all the data is written to the file
.hprofand analysis is performed for the presence of memory leaks. If the leak is still detected, the application will send you a notification of how this happens. It is recommended to use this application before release in production.
Android Studio also has a handy tool for detecting memory leaks. If there is a suspicion that some of the code in your application may cause a leak, then you can do the following:
- Compile and run the debug build on the emulator or device connected to your computer;
- Go to the suspicious operation, then go back to the previous action, which will output a suspicious operation from the task stack;
- In Android Studio, open Android Monitor window → Memory section and click on the Start GC button. Then press the button
Dump Java Heap;
4.After clicking the button,
Dump Java Heap Android Studio opens the file
.hprof. There are several ways to check for memory leaks through this file. You can use Analyzer Tasks in the upper right corner to automatically detect leaks. Alternatively, you can switch to mode
Tree Viewand find an action that should be disabled. We check the data
Total Count, and if we find differences in the data, it means that somewhere there is a memory leak.
5.Once the leak was discovered, you need to check the link tree and find out which object it is calling.
What are the common leakage patterns?
There are many reasons why there is a memory leak in Android. But they can all be classified into three categories.
- memory leaks initiated by a static link;
- memory leaks initiated by the workflow;
- just a leak.
You can download the SinsOfMemoryLeaks application , which will help you determine where the leak occurs.
In the Leak branch, you will see the reasons for the memory leak. This application can also be run on a device or emulator and use the above mentioned tools to track leaks. In the branch
FIXEDyou can see tips on how to fix leaks. After correction, the procedure can be repeated anew to finally make sure that the leaks are corrected. Each of the application branches has different application IDs, so you can install them on one device and check the readings at the same time.
And now we will quickly walk through all kinds of leaks.
Memory leaks initiated by a static link
A static link is maintained as long as your application is in memory. Operations have their own life cycles, which stop and begin when the application is running. If you access an operation directly or indirectly from a static link, the garbage collector will not clear the memory that is left after the operation is completed. The memory occupied by a particular operation can vary from a few kilobytes to several megabytes, depending on the state of the application. If it has a large hierarchy of views or high-resolution images, this can lead to a leak of large amounts of memory.
Some features of leaks for this category:
- leak for static submission ;
- A leak for a static variable ;
- leakage when using a singleton ;
- a static instance of the internal class .
Memory leaks initiated by the worker process
The workflow can also work longer than necessary. If you reference operations directly or indirectly from a worker thread that lives longer than the operations themselves, this will cause a memory leak. Some features of leaks for this category:
The same principle applies to flows such as
Just a leak
Every time you start a workflow from an operation, you are responsible for managing the flow. Because the worker thread can run longer than the operation itself, you need to stop it when the action is terminated. If this is not done, there is a possibility of a memory leak in the workflow. As in this repository .
What is the impact of a particular leak?
Ideally, you should avoid writing code that can cause a memory leak, and fix any leaks that exist in the application. But in fact, if you need to work with the old code base and determine the priorities of tasks, including fixing memory leaks, you can assess the severity in the following aspects.
How big is the memory leak?
Not all memory leaks are the same. Some leaks can be several kilobytes, and some – several megabytes. This can be determined using the tools presented above and decide whether the size of the leaked memory has a critical value for user devices.
How long does the leak last?
Some leaks through the worker thread live as long as this thread works. In this case, you need to learn how long this thread lives. In the example application above, there are endless loops in the workflow, so they constantly keep in memory the object that spawns the leak. But in fact, most workflows perform simple tasks, such as accessing the file system or performing network calls that are either short-lived or time-out-limited.
How many objects are there in the leak?
In some cases, the leak generates only one object, for example, one of the static link examples shown in the SinsOfMemoryLeaks application. As soon as a new action is created, it will begin to reference the new operation. The old leak will be cleaned by the garbage collector. Thus, the maximum leakage is always equal to the size of a single operation instance. However, other leaks continue to leak into new objects as they are created. In the example of Leaking Threads, the activity skips one thread each time it is created. Therefore, if you rotate the device 20 times, the leak will be 20 workflows. This will end very sadly, since the application will fill all available memory on the device.
How to fix and prevent leaks
Look at how typical memory leaks are eliminated in this branch of the repository . Solutions can be summarized to the following points:
- You need to be very careful when deciding to install a static variable for the workflow. Is it really necessary? Perhaps this variable refers to the process directly or indirectly (reference to an internal class object, attached screen, etc.)? If so, is it possible to purge the send to the process using the function
- If it was decided to transfer the operation as a singleton or
x-manager, you need to understand what another object does with the action instance. You need to clear the link (set to null), if necessary, using the function for this process
- When creating a class within a process, try to make it static as much as possible. Internal classes and anonymous classes have an implicit reference to the parent class. Therefore, if an instance of an internal / anonymous class lives longer than the parent class, problems may arise. For example, if you create an anonymous class
runnableand pass it to a worker thread or an anonymous handler class and use it to transfer tasks to another thread, there is a risk of leakage of the contained class object. To avoid the risk of leakage, you need to use a static class, rather than an internal / anonymous class.
- If you write a singleton or a
x-managerclass, you need to keep a reference to the listener instance (English “listener”). In this case, you do not control what happens with the link (deleted by the user of the class or not). In this case, you can use
WeakReferenceto create a reference to an instance of the listener.
WeakReferenceDo not interfere with the garbage collector to perform their actions. Although this function is great for preventing memory leaks, it can also cause a side effect, because there is no guarantee that the reference object is active when necessary. Therefore, it is recommended to use it as a last resort to fix memory leaks.
- You always need to terminate workflows initiated by the function
Be sure to check the sample code for typical memory leaks and how to avoid them in the repository on Github .