-
Notifications
You must be signed in to change notification settings - Fork 11
GObject lifetime management in Java‐GI
GObject lifetime is managed in Java-GI with a global “cache” of all objects that are used in the Java application. It uses toggle references and cleaners to keep track of, and dispose, instances.
The main idea of this functionality is that the Java code doesn’t manually ref
and unref
GObject instances. Java-GI maintains one single reference to its GObject instances “behind the scenes”, and only allows garbage collection for instances that are not used any more: neither in the Java program, or anywhere in native code.
The InstanceCache class contains a global static HashMap of all active GObject instances in Java, indexed by memory address. The value of each HashMap entry is either a strong or a weak reference.
A strong reference is an ordinary Java variable. As long as it is in the HashMap, the value is not garbage-collected. A weak reference however, can be garbage-collected (it becomes null
). See WeakReference (Java SE 23 & JDK 23) for more information about weak references.
GObjects are reference-counted. A new object has refcount 1. The refcount is increased with g_object_ref
, and decreased with g_object_unref
. When the refcount reaches 0, the object is disposed.
A toggle reference is a special reference that will emit a ToggleNotify signal in two specific circumstances:
- The toggle reference has become the last remaining reference for this instance (refcount has been set to 1)
- The toggle refernence is no longer the last reference; there are now other references too (refcount was 1, but not anymore)
The reason and proposed API for toggle references is explained here, though Java-GI uses them in a slightly different way than described in that email.
A toggle reference is enabled with g_object_add_toggle_ref
. This increases the refcount (just like g_object_ref
), so you need to do an unref
immediately afterwards to set the refcount back to 1, effectively "replacing" the normal ref with a toggle ref:
g_object_add_toggle_ref(obj, toggle_notify_cb, NULL);
g_object_unref(obj);
For documentation of the ToggleNotify event, see https://docs.gtk.org/gobject/callback.ToggleNotify.html
In the InstanceCache, when a ToggleNotify event is received, the entry in the HashMap is changed from weak to strong and vice versa:
- Last remaining reference: maintain a weak ref
- Not the last reference: maintain a strong ref
If implemented with two HashMaps (one with strong and one with weak references), the instance would move between the two maps.
All instances that are cached in the HashMap have a Cleaner attached. The Cleaner triggers an action when the instance is garbage-collected: It calls g_object_remove_toggle_ref
, and removes the entry from the HashMap.
Now the reasoning behind it, is as follows:
- A strong reference will keep the instance alive. Garbage collection can only happen when the HashMap contains a weak reference to it.
- The reference is only weak, when it is the last remaining reference (refcount = 1).
- Therefore, by removing the toggle ref, the Cleaner action causes the native GObject instance to be disposed, because the refcount becomes 0.
- The entry in the HashMap has now become a null pointer, and it can be removed from the map.
Let's track the lifetime of the gobject
variable in the following example code:
void addWidget(Box container) {
var gobject = new Label(""); // 1
container.append(gobject); // 2
} // 3
void main() {
var container = new Box();
addWidget(container);
} // 4
What happens at the numbered places:
1: A new GObject is constructed in Java code.
The Java code owns an instance (the application is actively using the instance), but it is not referenced from anywhere else: At this stage, the refcount is 1, and the HashMap maintains a weak reference.
2: The GObject is added to a container (in native memory).
Java code still uses the instance, but something else (the container) added a reference. The refcount is now 2. The HashMap entry "toggles" to a strong reference.
3: The GObject goes “out of scope” in Java code. It is still in the container, but it’s not directly reachable from Java code anymore.
Nothing happens: The GObject instance in Java is not garbage-collected, because the HashMap has a strong reference.
4: Later on, the container type is disposed.
The following happens:
- The container class will unref its children, so the refcount of our GObject drops back to 1.
- A ToggleNotify signal is emitted, and the reference in the HashMap is changed to a weak reference.
- With the strong reference replaced by a weak reference, the GC now garbage-collects the Java instance.
- The Cleaner runs
g_object_remove_toggle_ref
. The refcount of the GObject drops to 0 and it is disposed.
The HashMap in Java-GI keeps the Java instance alive as long as it is used, be it in Java code or in native code (like the container in the above example). Java-GI also ensures that when the Java object is retrieved again from the native container at a later point in time, it is the same Java object. Why? Because the Java wrappers around native functions never create a new Java object directly. They request a Java object from the InstanceCache, and they return the cached object. The InstanceCache creates a new instance only if it didn't exist yet for the requested memory address.
This allows Java developers to create a subclass with additional fields (that only exist in Java, not in native memory), store the object in a native container, forget about it, retrieve it later, and the Java fields will retain their values. This is possible because Java-GI kept the Java object alive in the InstanceCache while it was stored in the native container.
In the case described above (adding a GtkLabel to a GtkBox with gtk_box_append
) there is no ownership transfer. The GtkBox increases the ref count, and later decreases it again. The gtk_box_append
parameter has transfer-ownership="none"
.
In situations when the ownership is transferred (transfer-ownership="full"
), the native code will not increase the ref count, but they will decrease it later, when they are done. This would undo the toggle reference that Java maintains. So when a GObject instance is passed into a method with transfer-ownership="full"
, a call to g_object_ref
is generated by Java-GI, right before the native function call, to increase the refcount by 1.
Likewise, in a callback method, a call to g_object_ref
is inserted before a GObject is returned with transfer-ownership="full"
.
The reason Java-GI maintains a reference, even when the ownership has been transferred, is because the object can still be used in Java code. If it was removed from the cache, and the native function immediately calls unref
and destroys the object, the Java program could end up with a null pointer:
var object = ...; // my GObject instance
doSomething(object); // transfers ownership, calls unref(), and now the instance is disposed
object.method(); // critical error: object is NULL in native memory
The extra ref
call by Java-GI prevents the native object from being disposed in this scenario.
Floating references are a C convenience API, and are documented here. All classes derived from GInitiallyUnowned (notably GtkWidget) have floating references, but there are a few others (including GVariant). They are considered highly problematic for language bindings, so bindings are advised to immediately "sink" floating references when they are created. Java-GI therefore calls refSink()
on all new objects in the InstanceCache when they are known to initially have a floating reference. The refSink()
call "sinks" the floating reference, changing it into a regular reference.
-
Toggle references only work in the context of one language binding. An application cannot use the same GObject instance with multiple language bindings that use toggle references.
-
Just like all reference counting schemes, GObject suffers from cyclic references. This is the case in Java-GI too.
-
Callbacks in Java-GI (signal handlers, for example) are called from native code using an upcall stub, a natively allocated function pointer. (Upcalls and upcall stubs are explained here in great detail.) When an upcall stub is allocated, the corresponding Java codeblock and every object referenced by it cannot be garbage-collected until the stub is deallocated again. As a result, a signal callback that refers to the source of the signal can become an uncollectable reference cycle, until the signal is manually disconnected by the application author.