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 V8Object
s 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.
Top comments (1)
Very happy to see this.
And, thank you for writing this post, I didn't know about J2V8.