Kotlin/Native Memory Management Roadmap
TL;DR: The current automatic memory management implementation in Kotlin/Native has limitations when it comes to concurrency and we are working on a replacement. Existing code will continue to work and will be supported. Read on for the full story.
A bit of history
Kotlin/Native is designed to be the Kotlin solution for smooth integration with native platform-specific environments. In essence, its vision is to become to C-compatible languages what Kotlin/JVM is to JVM languages – a pragmatic, concise, safe, and tooling-friendly language for writing code. A very important part of this story is the Objective-C ecosystem of frameworks on various Apple platforms. Kotlin/Native support for the Objective-C ecosystem makes it possible to efficiently share Kotlin code between mobile applications with the ability to naturally and precisely use all the vendor APIs in a way that is hard or impossible to achieve with JVM-based solutions.
When the Kotlin/Native project started back in 2016, it was necessary to devise a memory management scheme. With Objective-C interoperability on the table, a lot of thought and attention was paid to the way automated memory management works there – using reference counting. We even experimented with an approach that completely mimicked that model, which would have offered the benefit of providing the most seamless fit. However, in the Objective-C ecosystem, object graphs with cycles are not managed automatically by the runtime. Programmers have to identify cyclic references and mark them in a special way in the source code. Having toyed with this approach, we quickly came to the conclusion that it comes into stark conflict with the core Kotlin philosophy of making development more enjoyable. Kotlin does require developers to be more explicit where this precision is needed for safety, but not where this extra code is just boilerplate that could have been managed by the language. Still, the reference-counting memory manager was easy to write to get the Kotlin/Native project started, and a cyclic garbage collector based on a trial-deletion algorithm was added to provide the kind of development experience that Kotlin programmers expect.
As the Kotlin/Native project matured and became more widely adopted, the limitations of such a reference counting-based automated memory management scheme started to become more apparent. For one thing, it’s hard to get high throughput for memory allocation-intensive applications. But while performance is important, it is not the sole factor in Kotlin’s design.
The limitations become worse, though, when you throw multithreading and concurrency into the mix. In all the ecosystems where fully automated reference-counting memory management is successfully used (most notably in Python), concurrency is severely restricted via something like the “Global interpreter lock” mechanism. This approach is not an option for Kotlin. It is imperative for mobile applications to be able to offload CPU-intensive operations into background threads that run in parallel with the main thread.
The current approach
To tackle this, a unique set of restrictions was developed for Kotlin/Native both to make it efficient at running single-thread code and to make it possible to share data between threads. The requirement was added that the object graph must first be frozen to prevent its modification. Only then it could be shared with other threads. Alternatively, it could be fully transferred to another thread as a detached object graph if no references to it remain in the original thread. As a bonus, by only sharing immutable data, the dreaded problem of “shared mutable state” was avoided for the most part. This scheme worked well, for a time.
While conceptually appealing, the current memory management approach in Kotlin/Native has a number of deficiencies that hinder wider Kotlin/Native adoption. Mobile developers are used to being able to freely share their objects between threads, and they have already developed a number of approaches and architectural patterns to avoid data races while doing so. It is possible to write efficient applications that do not block the main thread using Kotlin/Native, as many early adopters have shown, but the ability to do so comes with a steep learning curve.
There’s also another aspect of Kotlin – the goal of being able to share Kotlin code between platforms. However, some concurrent code, even if it is safe and race-free to begin with, is virtually impossible to share between Kotlin/JVM and Kotlin/Native. In particular, various concurrent data structures and synchronization primitives, which could be both generic and domain-specific, turned out to be notoriously hard to share between the two.
We encountered particular challenges when we attempted to implement multithreaded kotlinx.coroutines for Kotlin/Native. Synchronization primitives must internally share a mutable state, which is supported in Kotlin/Native via special atomic references. Yet the existing memory management algorithm does not track cycles through such references. Even after a considerable amount of work, it still suffers from memory leaks in some concurrent execution scenarios, and we don’t have a clear solution to address them.
New memory manager for Kotlin/Native
To solve these problems, we’ve started working on an alternative memory manager for Kotlin/Native that would allow us to lift restrictions on object sharing in Kotlin/Native, automatically track and reclaim all unused Kotlin memory, improve performance, and provide fully leak-free concurrent programming primitives that are safe and don’t require any special management or annotations from the developers. The new memory manager will be used for the whole compiled binary. We plan to introduce it in a way that is mostly compatible with existing code, so code that is currently working will continue to work. Namely, we plan to continue to support object freezing as a safety mechanism for race-free data sharing, and we will be looking at ways to improve Kotlin’s approach to working with immutable data in the whole Kotlin language, not just in Kotlin/Native. Existing annotations that are related to memory management will have an appropriate behavior with the new memory manager to ensure that old code still works. Meanwhile, we’ll continue to support the existing memory manager, and we’ll release multithreaded libraries for Kotlin/Native so you can develop your applications on top of them.
We will share more details in the future as the project develops and as we finalize various design decisions. Stay tuned, and have fun with Kotlin!