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, there is a common mistake made by beginners: 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:
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//...
activity = this
}
//...
companion object {
// DON'T DO THIS! It is a huge memory leak
var activity: MainActivity? = null
}
}
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 MainActivity:
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//...
// Be careful, we leak a reference to `this`
logError = {
Log.e(
this::class.simpleName,
it.message
)
}
}
//...
companion object {
// DON'T DO THIS! A memory leak
var logError: ((Throwable) -> Unit)? = null
}
}
However, problems with memory can be much more subtle. Take a look at the stack implementation below[^50_1]:
class Stack {
private var elements: Array<Any?> =
arrayOfNulls(DEFAULT_INITIAL_CAPACITY)
private var size = 0
fun push(e: Any) {
ensureCapacity()
elements[size++] = e
}
fun pop(): Any? {
if (size == 0) {
throw EmptyStackException()
}
return elements[--size]
}
private fun ensureCapacity() {
if (elements.size == size) {
elements = elements.copyOf(2 * size + 1)
}
}
companion object {
private const val DEFAULT_INITIAL_CAPACITY = 16
}
}
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:
fun pop(): Any? {
if (size == 0)
throw EmptyStackException()
val elem = elements[--size]
elements[size] = null
return elem
}
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:
fun <T> mutableLazy(
initializer: () -> T
): ReadWriteProperty<Any?, T> =
MutableLazy(initializer)
private class MutableLazy<T>(
val initializer: () -> T
) : ReadWriteProperty<Any?, T> {
private var value: T? = null
private var initialized = false
override fun getValue(
thisRef: Any?,
property: KProperty<*>
): T {
synchronized(this) {
if (!initialized) {
value = initializer()
initialized = true
}
return value as T
}
}
override fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: T
) {
synchronized(this) {
this.value = value
initialized = true
}
}
}
// usage
var game: Game? by mutableLazy { readGameFromSave() }
fun setUpActions() {
startNewGameButton.setOnClickListener {
game = makeNewGame()
startGame()
}
resumeGameButton.setOnClickListener {
startGame()
}
}
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:
fun <T> mutableLazy(
initializer: () -> T
): ReadWriteProperty<Any?, T> =
MutableLazy(initializer)
private class MutableLazy<T>(
var initializer: (() -> T)?
) : ReadWriteProperty<Any?, T> {
private var value: T? = null
override fun getValue(
thisRef: Any?,
property: KProperty<*>
): T {
synchronized(this) {
val initializer = initializer
if (initializer != null) {
value = initializer()
this.initializer = null
}
return value as T
}
}
override fun setValue(
thisRef: Any?,
property: KProperty<*>,
value: T
) {
synchronized(this) {
this.value = value
this.initializer = null
}
}
}
When we set the initializer to null, the previous value can be recycled by the GC.
How important is this optimization? Well, we don't know. It depends on what is captured by our lambda expression. It might be insignificant in most cases, but in others it might be a costly memory leak. That is why we should remember to replace unneeded references with null, especially in general-purpose tools. Stack and MutableLazy are such tools. For such tools, we should care more about optimization, especially if we’re 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:
private class SynchronizedLazyImpl<out T>(
initializer: () -> T, lock: Any? = null
) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
private var _value: Any? = UNINITIALIZED_VALUE
private val lock = lock ?: this
override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}
return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
override fun isInitialized(): Boolean =
_value !== UNINITIALIZED_VALUE
override fun toString(): String =
if (isInitialized()) value.toString()
else "Lazy value not initialized yet."
private fun writeReplace(): Any =
InitializedLazyImpl(value)
}
To free memory as soon as possible, it is useful to define your variables in a narrower scope and not capture them (Item 4: Minimize the scope of variables). A variable in a function (or lambda expression) is garbage collected when this function completes (or when this variable is not needed anymore). A property is garbage collected when a class instance is garbage collected. We should avoid keeping heavy values in top-level or object properties because these are never garbage collected.
It might also be useful to use weak references to reference views. This is a popular trick on Android. A weak reference does not stop the GC from collecting the object, so it can be collected as soon as this element is not visible anymore. This is very useful if we assume that we are not interested in references to views that are not displayed anymore.
class BaseActivity: Activity() {
private val errorDialog = WeakReference<Dialog?>(null)
// ...
}
The big problem is that memory leaks are sometimes hard to predict and don’t manifest themselves until the application crashes. 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.
[^50_1]: Example inspired by the book Effective Java by Joshua Bloch.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.
After working as a Java engineer for 8 years in various French IT companies, I moved to mobile application development on iOS and Android in 2012. In 2015, I decided to focus on Android and joined i-BP, an IT department of the French banking group BPCE, as Android expert. I am now passionate about Android, clean code, and, of course, Kotlin programming.
I am a mobile and web developer with over 10 years of experience. Currently, I’m specializing in mobile development (both native and cross-platform), but I was working as a frontend and backend developer as well, so I know the development process from each side.
I’m working as a Head of Mobile Development at Mews, so creating the architecture, code reviewing, mentoring, and integrating best practices is a part of my everyday job.
Currently, I’m mainly interested in Flutter. We have an application written in Flutter that is successfully running in production for more than a year.
If you need help with defining the right architecture for your mobile app (both from back-end and front-end sides) or looking for a mentor / code reviewer, feel free to contact me in LinkedIn or Twitter and let’s have a talk.