Effective Java in Kotlin, item 7: Eliminate obsolete object references

Marcin Moskala
Kt. Academy
Published in
6 min readAug 8, 2018

--

Programmers that were risen on languages with automatic memory management (like Java, in which Garbage Collector [GC] does all the job) rarely think about freeing objects. It leads to memory leaks and in some cases to OutOfMemoryError. The single most important rule is that object that is not used should be freed. Let’s see an example from the book (moved to Kotlin) which is a stack implementation:

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]
}

/**
* Ensure space for at least one more element, roughly
* doubling the capacity each time the array needs to grow.
*/
private fun ensureCapacity() {
if (elements.size == size)
elements = Arrays.copyOf(elements, 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 size, but we don’t free element on the array. Let’s say that we had 1000 elements on the stack, and we poped nearly all of them one after another and our size is now equal to 1. We should have only one element, and we can access only one element, but our stack still holds 1000 elements and doesn’t allow GC to destroy them. More such problems and we might have OutOfMemoryError. How can we fix it? The solution is simple:

fun pop(): Any? {
if (size == 0)
throw EmptyStackException()
val elem = elements[--size]
elements[size] = null
return
elem
}

This was a very rare example and a huge mistake, but there are everyday objects we use that profit or can profit from this rule. Let’s say that we need mutableLazy property delegate. It should work just like lazy, but it should also allow property state mutation. I can make 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()
}
}

Above implementation of mutableLazy is correct, but it has one flaw: initializer is not deleted after usage. It means that it is held as long as the reference to an instance of MutableLazy exist 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 initializer to null, previous value can be recycled by GC.

How important is this optimization? Not so important to bother for rarely used objects. There is a saying that premature optimization is a source of pure evil. Although it is good to set null to unused objects when it doesn’t cost you much to do it. Especially when it is a function type (which is often an anonymous class in Kotlin/JVM) or when it is an unknown class (Any or generic type. Example is above Stack which might have been used to hold heavy objects.). We should care about deeper optimizations when we do general tools, and especially libraries. 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)
}

The general rule is that:

When we hold state, we should always have memory management in our minds. Though, before changing implementation, we should find the best trade-off for our project, having in mind not only memory and performance but also readability and scalability of our solution. When we do an application, in most cases readability is more important. When we develop a library, generally performance and memory is more important.

There are a few common sources of memory leaks we need to discuss. First of all, caches hold objects that might never be used. This is the idea behind caches, but it won't help us when we suffer from out-of-memory-error. The solution is to use weak references. Objects still can be collected by GC if memory is needed, but often they will exist and will be used.

Another common problem is a problem of callbacks and listeners. Developers often register them and don’t deregister them when they are not necessary anymore. Here again, the solution is to register callbacks using a weak reference.

The big problem is that memory leaks are sometimes hard to predict and do not manifest themselves until some point when the application crashes. This is why we should search for them using special tools. The most basic tool is the heap profiler. We also have some libraries that help in the search for data leaks. For instance, a popular library for Android is LeakCanary which shows a notification whenever memory leak is detected.

About the author

Marcin Moskała (@marcinmoskala) is a trainer and consultant, currently concentrating on giving Kotlin in Android and advanced Kotlin workshops (fill out the form, so we can talk about your needs). He is also a speaker, author of articles and a book about Android development in Kotlin.

Do you need a Kotlin workshop? Visit our website to see what we can do for you.

To be up-to-date with great news on Kt. Academy, subscribe to the newsletter, observe Twitter and follow us on medium.

--

--

Kt. Academy creator, co-author of Android Development with Kotlin, author of open-source libraries, community activist. http://marcinmoskala.com/