blob: 7556182541bd8bcf4f42319c8e69d21b81a1cf89 [file] [log] [blame] [view] [edit]
## 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.
```dart
// Construct the object.
final hello = 'Hello'.toJString();
// Use it.
print(hello);
// Eagerly release it!
hello.release();
```
To make releasing easier, we can use
[`Arena`](https://pub.dev/documentation/ffi/latest/ffi/Arena-class.html)s.
First, create an `Arena` via
[`using`](https://pub.dev/documentation/ffi/latest/ffi/using.html). Then
register the object to be released at the end of the callback.
```dart
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 like `List` or `Map`. Use Java
collections such as `JList` or `JMap` instead.
```dart
// 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 global `JObject`s. This prevents their
references from being released.
- When an original Java object is no longer needed, set `releaseOriginal` to
`true` during conversion to Dart equivalents or casting.
```dart
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:
```dart
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.
```mermaid
graph TD;
f-->|holds in Dart|foo;
foo-->|holds in Java|bar;
bar-->|holds in Java|f;
```
To prevent cycles, use
[`WeakReference`](https://api.dart.dev/dart-core/WeakReference-class.html)s.
```dart
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.
```mermaid
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`.
>
> ```dart
> 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));
> // ...
> }
> ```