: code tutorial (v3)

Lesson 3: The Proxy Object Lifecycle

Introduction

C++ proxy objects represent Java objects that live inside the JVM. Java uses a garbage collector to clean up unused objects whereas C++ has destructors that are invoked by the compiler when objects go out of scope.

That's a pretty big difference in how the two languages manage their objects' lifecycle, so you can reasonably wonder how these two approaches work together. It's really pretty simple.

C++ Is in the Driver Seat... Mostly

When you use C++ proxy types to control Java objects in an embedded JVM you always start out with C++ code. You create a C++ object and you expect that object to either be initialized to a Java null value or to the result of a Java constructor invocation. The former case is trivial as there is no Java object to keep track of, so let's focus on the latter.

When you create a C++ proxy object that maintains a reference to a Java object, the C++ object becomes the owner of a reference to the Java object. That reference will prevent the Java object from being garbage-collected for as long as the C++ object exists.

When the C++ object is destroyed it explicitly releases its hold on the Java object. While the C++ object is "dead" the moment its destructor has executed, the same is not true for the Java object which it referenced. Assuming that there were no other references to the same Java object, it merely becomes eligible for garbage collection. It sits there in the Java heap until the Java runtime decides to collect it.

So far so good, but what happens when you pass proxy objects around? Let's look at the following C++ snippet that uses the java::util::Hashtable proxy type:

// open a scope
{
    // create a proxy Hashtable
    Hashtable    ht1( 15 );
    // assign it to another proxy instance
    Hashtable    ht2 = ht1;
// close the scope
}

In the above snippet we create two proxy instances for Java's Hashtable type. The first one is created by invoking one of the Java constructors. The second instance is assigned the first instance's value. Finally, both instances go out of scope resulting in their C++ destructors being invoked by the compiler.

After the first object ht1 is created we have our simple case of a proxy object owning a corresponding Java object. The assignment to ht2 causes that proxy instance to reference the same Java Hashtable instance via a duplicated reference. Essentially, there are now two references to one Java object. Each C++ object owns its own Java object reference that happens to refer to the same Java object.

With the the closing curly brace both C++ instances go out of scope and their destructors are invoked. A proxy type destructor has just one job: to release the Java reference it holds. C++ objects are cleaned up in reverse order of instantiation, so ht2 releases its Java reference first. This makes no difference to the Java object yet because it is still referenced by ht1. Once ht1's destructor has completed though, all references to the Java instance are gone and the hashtable becomes eligible for garbage collection. "Eligible for GC" does not mean that it is immediately deleted though. Depending on the garbage collection algorithm being used by your JVM, the object could remain alive for quite a while. This usually does not matter to you unless you rely on Java object finalization in addition to garbage collection.

So you see that the lifetime of the C++ proxy objects provides a lifetime guarantee for the referenced Java object. While there is at least one proxy instance referring to a Java object, the Java object is guaranteed to remain alive. The Java object may remain alive longer, but that is up to your particular JVM.

Why Did We Say "Mostly"?

It's totally possible to create memory leaks in Java by registering objects with so-called garbage collection roots. You could for example have a Java class with a static object cache member and you register some objects with that cache. Unless you used weak references, the registered objects can probably never be garbage collected because the object cache keeps holding on to them and the object cache itself is a static member of a class which is not eligible for garbage collection itself unless its class loader becomes eligible.

The same can happen when you declare global/static C++ proxy instances or when you allocate a proxy instance on the dynamic heap with new and then forget to call delete. These objects exist for the lifetime of the application, so the referenced Java objects will never become eligible for garbage collection.

In Summary

The proxy types have been carefully designed to manage the referenced Java objects for you. Constructors, assignment operators and the destructor work in concert to shield you from ever having to manage Java object references directly.

Assigning between proxy objects duplicates the source object's Java reference in the destination object. Both source and destination will contain valid references to the same Java object after the assignment completes.

For most practical purposes you can treat the life cycle of a Java object as the union of the lifecycles of all C++ proxy instances that are holding references to the object. In other words: a Java object usually "dies" soon after the last C++ object that held a reference to it has been destroyed.

Avoid global and heap-allocated proxy instances if you can. It's too easy to unintentionally hold on to a Java object forever.