Memory efficiency with sealed object

Sergey Opivalov
AndroidPub
Published in
4 min readFeb 14, 2019

Hello, dear readers

You could say — wow, what a clickbait title… but no, this article is mostly my personal opinion about Kotlin’s sealed class using and case i was faced. Feel free to leave comments and discuss the topic.

During the regular code review i pay attention at further code:

I was confused about that children of sealed class — one of them defined as object and another one as class. After discussion with author of PR I’ve resolved his motivation — “StateB entity doesn’t has any payload, it’s literally just a dummy class, so for reducing number of allocated objects, I’ve define it as object”. Memory efficiency as a reason. Reduced number of allocated objects => reduced number of GC runs => app performance boosted. Sounds good. Let’s dive deep and take a look at possible drawbacks of this approach.

Thread-safety

First of all I had to refresh my knowledge of Kotlin’s object, and first point that i was interested in was thread-safety of singleton, defined as object. And … https://kotlinlang.org/docs/reference/object-declarations.html yes, Kotlin object is thread-safe lazy singleton

Lifecycle

Next topic — lifecycle. When object will be garbage-collected? I’m decompiled StateB and got the following:

As you see — instance reference stored in static field. To be honest in JVM language it’s pretty normal to implement singletons like this. Everyone did it. But you must take into account, that objects with reference in static variables will never be garbage-collected since refCount will never become 0. So that mean it will live «forever».

From the one point — it’s ok, enum works similarly. But from another point — State it’s just a «state object», it suppose to die young (will be garbage collected during next GC run after allocation).

Consistency

enum vs sealed class

enum — singletons

sealed class — not described exactly, optional singletons i would say. And some more differences.

When i saw object as inheritor of sealed class my first though: “If you need singleton behaviour why would you not using enum"

As mentioned before, if you are using construction from topic of this article, one of your states will live «forever» and another one will die after using. Nothing special but it really differs from my thoughts about consistency and engineering culture.

Public API

sealed class was designed for introduce pattern-matching in Kotlin. So public API while using sealed class is parent class declaration. As for me when i see the reference at sealed class I suppose that all inheritors are just regular classes with only one goal — describe state of something and die. I’m not expecting an object because public API is sealed class, not sealed object, right?

You can oppose to me and throw a link https://kotlinlang.org/docs/reference/sealed-classes.html where we can see sealed class with inheritors defined as object. But i’m still thinking that it’s just example to show you it can be compiled, but not guide how to use sealed class

Most nasty thing that some clients code can rely at fact StateB is singleton. And after some refactoring you can change declaration of StateB to class, but completely forget about that fact. The opposite situation is also possible:

Now look, StateOwnerA mutating the state, and StateOwnerB just read the value. Everything is fine while state is StateA. But when state is StateB — mutation in one class will affecting at reading results in another class. That’s how you can easily shoot at your leg.

SOLID

Tricky point. We are not able to substitute all the occurrences of State in code at StateB and not to break behaviour of app. Since clients code can rely at fact that State is just class (while looking at public API), and when you replace it with singleton app behaviour can be broken. Thus we are violating Liskov substitutional principle.

Memory efficiency

Major point. There is Generational hypothesis https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/generations.html. TL;DR it’s empirical observation about fact that mostly objects in mostly apps die young.

That was done in 1980s. In simple words all the modern implementations of tracing GCs has very good optimised algorithms for clearing «young» and «old» generations of objects. That mean that you should not afraid to make new allocations when needed (within reason).

For making sure you can see at the chart below:

It’s a simple garbage-only benchmark that shows throughput of modern GC implementations. ~9Gb/sec no so bad, isn’t it? Once again — you can allocate and clear 9Gb of objects in second. Pretty much to satisfy almost all your requirements. That benchmark extreme, but rather synthetic so real values will depends on many factors.

And the last point, we are lucky guys and we have minSdk = 21, thus we are have ART. It has some significant advantages against Dalvik in terms of memory allocations and garbage collection. More info https://source.android.com/devices/tech/dalvik

Conclusion

Developers make mistakes… Apps full of mistakes… Most dangerous thing in fusing object and class as inheritors of sealed class it’s possibility to forget about specific behaviour of one of inheritors, start to abuse it and finally get a few funny hours of debugging. Let’s trying to guard ourselves from that kind of mistakes and keep our codebase consistent.

Developers make mistakes… Apps full of mistakes… Most dangerous thing in fusing object and class as inheritors of sealed class it’s possibility to forget about specific behaviour of one of inheritors, start to abuse it and finally get a few funny hours of debugging. Let’s trying to guard ourselves from that kind of mistakes and keep our codebase consistent.

--

--