How Kotlin helps you avoid memory leaks

Marcos Holgado
ProAndroidDev
Published in
6 min readFeb 22, 2019

--

Last week I gave a talk at MobOS about writing and automating performance tests in Android. As part of the talk I wanted to demonstrate how you could detect memory leaks during your integration tests. To prove that, I created an Activity using Kotlin that was supposed to leak memory but for some reason it didn’t. Was Kotlin helping me out without me knowing it?

Before I begin, the code of this article is available at the kotlin-mem-leak branch of my performance-test repo:

https://github.com/marcosholgado/performance-test/tree/kotlin-mem-leak

The whole premise was simple, I wanted to write an activity that would leak memory so I could detect that during an integration test. Because I was already using leakcanary I copied their sample Activity to recreate a memory leak. I removed some code from the sample and ended up with the following Java class.

The LeakActivity has a button that when pressed, creates a new Runnable that runs for 20 seconds. Because the Runnable is an anonymous class it has a hidden reference to the outer LeakActivity and if the activity gets destroyed before the thread finished (20 seconds after pressing the button) then the activity will leak. It won’t leak forever though, after those 20 seconds it would be available to be garbaged collected again.

Since I was writing my code in Kotlin I converted that Java class into Kotlin code which looked like this:

There is nothing special here, I’m taking advantage of lambdas to remove boilerplate from the Runnable and in theory everything should be the same right? I then wrote the following test using leakcanary and my own @LeakTest annotation to run their memory analyzer only in this test.

The test performs a button click and because that’s the only thing we are doing, the activity will get destroyed immediately after and creating a leak since we didn’t wait for 20 seconds.

If we try to execute the testLeaks test inside MyKLeakTest you will see that the tests passes meaning that we didn’t detect any memory leaks.

This result confused me a lot, at the end of the day I was replacing that anonymous Java class with an Anonymous inner class and since it was an instance of a functional Java interface I could use a lambda expression instead (more about single abstract methods or SAM conversions here).

I was so confused and felt so stupid that I tweeted this:

And got this reply which made me laugh. I wish my skills were up to that level :D

It is so easy to get trapped in this circle of “there’s nothing wrong but I can’t find what’s going on” that I went back to basics.

I wrote a new activity, same code but this time I kept it in Java. I changed the test to point to this new Activity, run it and this time… it failed. Things started to make a little more sense now. The Kotlin code had to be different to the Java code, something had happened there and there was just one place to look for it. The bytecode.

Analyzing LeakActivity.java

To begin with I analyzed the Dalvik Bytecode of the Java activity. To do that you have to analyze your apk via Build/Analyze APK... and then select from the classes.dex file the class you want to analyze.

We right click on the class and select Show Bytecode to get the Dalvik Bytecode of the class. I’ll just focus on the startAsyncWork method since we know it’s the place where the memory leak happens.

We know that an anonymous class keeps a reference to the outer class so we are going to be looking for that. In the bytecode above we create a new instance of LeakActivity$2 and we store it in v0 (line 10).

new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;

But what is LeakActivity$2? If we keep looking at our classes.dex file you will find it there.

So let’s see the Dalvik bytecode for that class. I have removed some code from the result that we don’t really care about.

The first interesting thing you can see is that this class implements Runnable.

# interfaces
.implements Ljava/lang/Runnable;

Like I said before this class should have a reference to the outer class so where is it? Just below the interface there is an instance field of type LeakActivity.

# instance fields
.field final synthetic
this$0:Lcom/marcosholgado/performancetest/LeakActivity;

And if we look at the constructor of our Runnable you will see that it takes one parameter, a LeakActivity.

.method constructor 
<init>(Lcom/marcosholgado/performancetest/LeakActivity;)V

Going back to the bytecode of LeakActivity where we were creating a LeakActivity$2 instance, you can see how it then uses that instance (stored in v0) to invoke the constructor that we’ve just seen to pass an instance of LeakActivity.

new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;invoke-direct {v0, p0},   
Lcom/marcosholgado/performancetest/LeakActivity$2;-><init>
(Lcom/marcosholgado/performancetest/LeakActivity;)V

So our LeakActivity.java class would indeed leak if it’s killed before the Runnable finishes because it has a reference to the activity and it would not be garbage collected at that point.

Analyzing KLeakActivity.kt

If we now look at the Dalvik bytecode of the KLeakActivity and we focus again in the startAsyncWork method we would get the following bytecode.

In this case, rather than creating a new instance, the bytecode is doing a sget-object operation which performs an object static field operation with the identified static field.

sget-object v0,         
Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1; -> INSTANCE:Lcom/marcosholgado/performancetest/KLeakActivity$startAsyncWork$work$1;

Going a bit deeper into the KLeakActivity$startAsyncWork$work$1 bytecode we can see that, like before, this class implements Runnable but now it has a static method that doesn’t need an instance of the outer class.

And that’s why my KLeakActivity wasn’t really leaking anything, by using a lambda (actually a SAM) rather than an anonymous inner class I wasn’t keeping a reference to my outer activity. But it wouldn’t be fair to say that this is something specific to Kotlin, if you are using Java8 lambdas the result is exactly the same.

If you want to know more about this, I highly recommend reading this article about lambda translations, but I will highlight this for you.

Lambdas like those in the above section can be translated to static methods, since they do not use the enclosing object instance in any way (do not refer to this, super, or members of the enclosing instance.) Collectively, we will refer to lambdas that use this, super, or capture members of the enclosing instance as instance-capturing lambdas.

Non-instance-capturing lambdas are translated to private, static methods. Instance-capturing lambdas are translated to private instance methods

So what is this about? Our Kotlin lambda is a non-instance-capturing lambda because is not using the enclosing object instance. However if we were using let’s say a field from the outer class, our lambda would then have a reference to the outer class and leak.

In the above example we are using the field test inside our Runnable hence having a reference to the outer activity and creating a memory leak. Looking again at the bytecode you can see how it now needs to pass an instance of KLeakActivity to our Runnable (line 9) since we are now using an instance-capture lambda.

That was everything, I hope this article will help you understand a little bit more about SAM, lambda translations and how you can safely use non-capturing lambdas without having to worry about memory leaks.

Remember that if you want to try this, all the code of this article is available at this repo.

I realize this is not a very straightforward topic so If you have any questions or if you think I messed up somewhere, please leave a comment or reach out on Twitter.

--

--

Senior Android Developer at DuckDuckGo. Speaker, Kotlin lover and I also fly planes. www.marcosholgado.com