Lifecycle topic
Lifecycle of Java objects
All Java classes generated by package:jnigen
extend JObject
. In JNI there
are several kinds of references: local, global, and weak global. Local
references are tied to a single thread. To enable seamless transfer of
JObject
s between isolates and safe usage in asynchronous code, JObject
s
always use global references. It's crucial to remember that there's a limit
of approximately 50,000 global references.
Once all references (in both Java and Dart) to an object are gone, Java's garbage collector (GC) can reclaim it.
Automatic reference management
JObject
s attach a native finalizer to their global references. Therefore, when
the Dart GC collects them, the underlying Java reference is released.
This approach works well for application code where a large number of references aren't created repeatedly. However, it's not ideal for packages because the usage patterns are unpredictable as you will never know how your users will use your package. This can lead to exceeding the global reference limit and application crashes.
Eagerly releasing references manually (recommended for packages)
Instead of waiting for Dart GC to release the JNI global references,
.release()
can be called on the JObject
s.
// Construct the object.
final hello = 'Hello'.toJString();
// Use it.
print(hello);
// Eagerly release it!
hello.release();
To make releasing easier, we can use
Arena
s.
First, create an Arena
via
using
. Then
register the object to be released at the end of the callback.
using((arena) {
final hello = 'Hello'.toJString()..releasedBy(arena);
final world = 'World'.toJString()..releasedBy(arena);
print(hello);
print(world);
});
// Both `hello` and `world` are now released.
Tips on keeping the number of global references low
-
Avoid storing
JObject
s in Dart collections likeList
orMap
. Use Java collections such asJList
orJMap
instead.// GOOD: final jstrings = JList(JString.type); using((arena) { final hello = 'Hello'.toJString()..releasedBy(arena); final world = 'World'.toJString()..releasedBy(arena); jstrings.add(hello); // Add to Java collection jstrings.add(world); // Add to Java collection }); print(jstrings.length); // prints 2.
This approach avoids storing individual references. References are created only when accessing elements and released afterward.
-
Minimize the use of
static
or globalJObject
s. This prevents their references from being released. -
When an original Java object is no longer needed, set
releaseOriginal
totrue
during conversion to Dart equivalents or casting.final foo = Foo(); final String string = foo.someJString().toDartString(releaseOriginal: true); final JInteger jint = foo.someJNumber().as(JInteger.type, releaseOriginal: true); final int dartInt = castedAsInteger.intValue(releaseOriginal: true); foo.release(); // All references are removed.
Lifecycle of interfaces implemented in Dart
Java interfaces can be implemented in Dart using .implement
. To ensure
accessiblity of the Dart implementations as long as the created Java object
exists, JNIgen transfers the ownership of these Dart objects to Java. When the
Java GC reclaims the implemented object, a message is sent to Dart to remove the
corresponding closures.
Cycles between Java and Dart GC's
One could create cycles between Dart and Java GC's when implementing interfaces. For example consider the following:
final foo = Foo();
foo.bar = Bar.implement($Bar(
f: () {
return foo;
}
));
In Dart, closure f
holds a reference to foo
, preventing its garbage
collection. In Java, foo
holds a reference to bar
, preventing its release
and thus preventing the closure's removal. This creates a cycle.
graph TD;
f-->|holds in Dart|foo;
foo-->|holds in Java|bar;
bar-->|holds in Java|f;
To prevent cycles, use
WeakReference
s.
final weakFoo = WeakReference(foo);
foo.bar = Bar.implement($Bar(
f: () {
final foo = weakFoo.target;
if (foo == null) {
throw StateError();
}
return foo;
}
));
The weak reference breaks the cycle, allowing the GCs to collect the objects.
graph TD;
f-.->|holds weakly in Dart|foo;
foo-->|holds in Java|bar;
bar-->|holds in Java|f;
Warning
Be careful about the closures accidentally overcapturing. To prevent
overcapturing, implement your logic in a separate function or create a class
that implements $Bar
.
final class BarImpl with $Bar {
final WeakReference<Foo> weakFoo;
BarImpl(this.weakFoo);
@override
void f() {
final foo = weakFoo.target;
if (foo == null) {
throw StateError();
}
return foo;
}
}
void main() {
final weakFoo = WeakReference(foo);
foo.bar = Bar.implement(BarImpl(weakFoo));
// ...
}
Libraries
- jnigen Java Differences Lifecycle Threading Interface Implementation
- This library exports a high level programmatic API to jnigen, the entry point of which is runJniGenTask function, which takes run configuration as a JniGenTask.