Effective Kotlin Item 50: Eliminate obsolete object references
Programmers who are used to languages with automatic memory management rarely think about freeing objects. In Java, for example, the Garbage Collector (GC) does this job. However, forgetting about memory management often leads to memory leaks (unnecessary memory consumption) and in some cases to the
OutOfMemoryError. The single most important rule is that we should not keep a reference to an object that is not useful anymore, especially if such an object is big in terms of memory or if there might be a lot of instances of such objects.
In Android, beginners make a very common mistake made: since a reference to
Activity (a concept similar to a window in a desktop application) is needed in many Android functionalities, they store it in a companion object or a top-level property for convenience:
Holding a reference to an activity in a companion object does not let the Garbage Collector release it if our application is running. Activities are heavy objects, so this is a huge memory leak. There are some ways to improve this situation, but it is best not to hold such resources statically at all. Manage dependencies properly instead of storing them statically. Also, notice that we might cause a memory leak when we hold an object that stores a reference to another one. In the example below, we hold a lambda function that captures a reference to the
However, problems with memory can be much more subtle. Take a look at the stack implementation below1:
Can you spot a problem here? Take a minute to think about it.
The problem is that when we pop, we just decrement the size of this stack, but we don’t free an element in the array. Let’s say that we have 1,000 elements on the stack and we pop nearly all of them, one after another, until the size of the stack is 1. Now, we can access only one element, and so we should hold only one element. However, our stack still holds 1,000 elements and doesn't allow the GC to destroy them. All these objects are wasting our memory. This is why they are called memory leaks. If these leaks accumulate, we might face an
OutOfMemoryError. How can we fix this implementation? A very simple solution is to set
null in the array when an object is not needed anymore:
We should recognize and release the values that are not needed anymore. This rule applies in a surprising number of classes. To see another example, let’s say that we need a
mutableLazy property delegate. It should work just like
lazy, but it should also allow property state mutation. I can define it using the following implementation:
The above implementation of
mutableLazy works correctly, but it has one flaw: the
initializer is not cleaned after usage. This means that it is held as long as the reference to an instance of
MutableLazy exists, even though it is not useful anymore. This is how
MutableLazy implementation can be improved:
When we set the initializer to
null, the previous value can be recycled by the GC.
How important is this optimization? It’s not so important for rarely used objects, but there is a saying that premature optimization is the root of all evil. However, it is good to replace unneeded references with
null, especially when the reference is a function type which can capture many variables, or when the reference is an unknown value. For example,
Stack from the above example might be used to hold heavy objects, so it is a general-purpose tool, but we don’t know how it will be used. For such tools, we should care more about optimization, especially when we are creating a library. For instance, in all 3 implementations of a lazy delegate from Kotlin stdlib, we can see that initializers are set to
null after usage:
The general rule is that when we hold a state, we should have memory management in mind. Before changing an implementation, we should always think of the best trade-off for our project, bearing in mind not only memory and performance but also the readability and scalability of our solution. Generally, readable code will also do fairly well in terms of performance or memory. Unreadable code is more likely to hide memory leaks or wasted CPU power. Sometimes, however, these two values conflict, but readability is more important in most cases. One case in which this is not true is when we are developing a general-purpose library because then performance and memory use are often considered more important.
There are a few common sources of memory leaks that we need to discuss. First, caches hold objects that might never be used. This is the idea behind caches, but it won't help us when we encounter an out-of-memory error. The solution is to use soft references. When we do so, objects can still be collected by the GC if memory is needed, but often they will still exist and will be used. Also, some other objects, such as a dialog on a screen, should be referenced using weak reference as it won’t be Garbage Collected anyway as long as it is displayed. Once it vanishes, we do not need to reference it anyway, so this is a perfect candidate for an object being referenced using a weak reference.
The big problem is that memory leaks are sometimes hard to predict and do not manifest themselves until the application crashes. This is especially true for Android apps, as there are stricter limits on memory usage than for other kinds of clients, like desktop. This is why we should search for leaks using special tools, the most basic of which is the heap profiler. There are also some libraries that help in the search for data leaks. For instance, a popular library for Android is LeakCanary, which shows a notification (to the developer) whenever a memory leak is detected.
It is important to remember that the need to free objects manually is pretty rare. In most cases, objects are freed anyway thanks to the scope, or when the objects holding them are cleaned up. The most important way to avoid cluttering memory is having variables defined in a local scope (Item 2: Minimize the scope of variables) and not storing possibly heavy data in top-level properties or object declarations (including companion objects).
Example inspired by the book Effective Java by Joshua Bloch.