Managing Object Lifetimes in Vala: Understanding Owned vs Unowned
Vala automates GObject reference counting at compile time, but cycles still leak. Learn how owned and unowned references work, with a parent–child example and verification steps.
08 Jun 2026, 06:59 UTC

When writing C code with GLib/GObject, the most common source of crashes is a mismatch between g_object_ref() and g_object_unref(). Miss one unref and you leak memory; call one too many and you get a use-after-free. Vala eliminates this class of bug by automating reference counting at compile time — but it is not a garbage collector. The compiler decides where to insert those C calls based on how you declare ownership.
The practical skill in Vala is not making memory disappear, but stating clearly who is responsible for an object at any moment. That is what the owned and unowned modifiers are for.
How Vala's ARC Works
Vala implements a form of Automatic Reference Counting (ARC). Unlike a tracing garbage collector, there is no background process scanning the heap. Instead, the compiler analyzes variable scopes and inserts the GLib reference calls into the generated C code.
By default, when you create an object you hold an owned reference. When that variable goes out of scope, the compiler emits g_object_unref. When you pass the object to another function or store it in a field, Vala decides whether to increment the reference count or pass the pointer without touching the count — and that decision depends on the ownership qualifiers you wrote.
Owned vs Unowned: Who Keeps the Object Alive
- Owned: The holder keeps the object alive. When the last owned reference disappears, the reference count reaches zero and the object is finalized. Assigning an owned reference to another owned variable increments the count; transferring it (for example, returning it) moves the responsibility.
- Unowned: A non-owning pointer. The count is not incremented, and the object may be destroyed while you still hold the pointer. This is the standard tool for back-references and caches.
Practical Example: Breaking a Parent–Child Cycle
A common pattern: a Document owns its Page objects, but each Page needs to reach back to its document.
public class Document : Object {
private Page main_page;
public Document () {
// Document takes ownership of the new Page
this.main_page = new Page (this);
}
}
public class Page : Object {
// 'unowned' prevents a circular reference
private unowned Document parent_doc;
public Page (Document doc) {
this.parent_doc = doc;
}
}If parent_doc were a normal (owned) field, the Document would hold the Page and the Page would hold the Document. Neither count would ever reach zero, and both objects would leak. Marking the back-reference unowned breaks the cycle: the Page can use the Document while it exists, but does not keep it alive.
Trade-offs and Limitations
- Circular references leak. Reference counting cannot collect cycles. Any object graph with loops needs at least one
unownededge (or an explicit disconnect) or the whole cycle leaks. - Atomic overhead. GObject reference counting uses atomic operations for thread safety. In tight loops over many small objects this cost is measurable, though rarely dominant.
- Misplaced
ownedtransfers. Explicitly transferring ownership with theownedkeyword at the wrong boundary can destroy an object while other code still uses it, or leak it if nobody takes responsibility. - C interoperability. When calling C libraries, you must know whether a returned reference is yours to free. Vala bindings express this with ownership annotations on the C side; getting them wrong reintroduces exactly the bugs Vala is meant to prevent.
Verifying the Behavior
Two practical checks, run from your project directory with the Vala toolchain installed:
- Inspect the generated C. Compile with
-Cto stop at the C stage:
Open the generatedvalac -C main.valamain.cand search forg_object_unref. You should see an unref emitted at the end of each scope that held an owned reference, and none for theunownedfield. - Check for leaks at runtime. Build a binary and run it under Valgrind (requires Valgrind installed; expect a slow run):
GLib itself keeps some one-time allocations alive, so focus on "definitely lost" blocks involving your own classes rather than chasing every reported byte.valgrind --leak-check=full ./your_program
Closing
Vala's memory model rewards one habit: whenever you store a reference to an object you did not create, ask whether you are keeping it alive or merely borrowing it. Default to owned, reach for unowned the moment two objects point at each other, and confirm the result with the generated C or a Valgrind run. That single discipline removes the most painful class of GObject bugs without giving up C-level performance.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.