DEV Community

Cover image for Elegant memory management with Kotlin and J2V8
Graham Borland for YNAB

Posted on

Elegant memory management with Kotlin and J2V8

At YNAB we have a cross-platform library written in TypeScript (compiled to JavaScript) which contains all of our shared business logic for Android, iOS and Web.

On Android we use J2V8 as our bridge into the JavaScript world; it's a nice Java wrapper around Google's V8 JavaScript engine. It works beautifully, but one of the challenges it brings is memory management. It's so tricky that the J2V8 maintainers wrote a blog post about it.

Because J2V8 bridges V8 and Java, three different memory models are in play. Both Java and JavaScript provide a managed memory model with their own GC. JNI / C++ which sits in the middle is completely unmanaged. This leads to a complex situation...

To cut a long story short, we have to explicitly release any JS objects we create in our Java/Kotlin code.

Remember to close the door

We can release these objects manually:

// create a JS object
val obj = V8Object(v8)

// do something with the object in JS land
obj.add("someProperty", 54321)
obj.executeJSFunction("someJSFunction", 42)

// now release it
obj.close()

But it's a bit of a pain having to remember to call close() for every object. V8Object implements the Closeable interface which means we can use Java's try-with-resources or Kotlin's use { } to take care of cleanup where we only have a single object to deal with.

V8Object(v8).use { obj ->
    obj.add("someProperty", 54321)
    obj.executeJSFunction("someJSFunction", 42)
}

It gets hairy when we need to track multiple JS objects, though. To help, J2V8 provides a MemoryManager. When we create one of these it starts tracking V8Object allocations while it is open, and releasing the MemoryManager causes all of those objects which were allocated during its lifetime to be released in turn.

val manager = MemoryManager(v8)

val obj1 = V8Object(v8)
val obj2 = V8Object(v8)

obj1.add("someProperty", 54321)
obj2.executeJSFunction("someJSFunction", 42)

manager.release()   // obj1 and obj2 are both released

It would be nice if we could use try-with-resources or use { } again here, to avoid the explicit call to manager.release(), but MemoryManager doesn't implement Closeable so we can't.

An elephant elegant solution

What we can do, though, is add a helper function which wraps all the MemoryManager stuff and provides a scope for allocating and safely cleaning up as many V8Objects as we like.

inline fun <T> V8.scope(body: () -> T) : T {
    val scope = MemoryManager(this)
    try {
        return body()
    } finally {
        scope.release()
    }
}

It has to be inline so that we don't interfere with any return value from the body lambda. And making it an extension function on V8 gives us this concise and elegant syntax.

v8.scope {
    val obj1 = V8Object(v8)
    val obj2 = V8Object(v8)

    obj1.add("someProperty", 54321)
    obj2.executeJSFunction("someJSFunction", 42)
}   // obj1 and obj2 are both released

Elephants never forget... and now, neither do we! This approach helps us to solve some of the memory-related pain points when mixing JavaScript and good old Java/Kotlin code, without too much boilerplate. We do have a keen eye on Kotlin multiplatform for the future, but our JavaScript shared library is serving us very nicely in the meantime.

The code is available on GitHub.

Oldest comments (1)

Collapse
 
piannaf profile image
Justin Mancinelli

We do have a keen eye on Kotlin multiplatform for the future, but our JavaScript shared library is serving us very nicely in the meantime.

Very happy to see this.

And, thank you for writing this post, I didn't know about J2V8.