Java Equivalents of Malloc(), new, free(), and delete

Memory management operators in Java

Posted by Mr.Humorous 🥘 on December 10, 2018

1. The delete operator in Java…?

Java has no direct equivalent of delete. That is, there is no call that you can make to tell the JVM to “deallocate this object”. It is up to the JVM to work out when an object is no longer referenced, and then deallocate the memory at its convenience.

Now if you’re a C++ programmer, at this point you might be thinking “that’s cool, but what about deconstructors”? In C++, if a class has a deconstructor, it is called at the point of invoking delete and allows any cleanup operations to be carried out. In Java, things are slightly different:

  • there’s no way to guarantee that any cleanup function will be called at the point of deallocation…
  • …but given Java’s automatic garbage collection, cleanup functions are generally much less necessary;
  • Java provides a mechanism called finalization, which is essentially an “emergency” cleanup function with a “weak” guarantee of being called;
  • Java’s finally mechanism provides another means of performing certain common cleanup functions (such as closing files).

An explicit deconstructor isn’t needed in Java to deallocate “subobjects”. In Java, if object A references object B, but nothing else does, then object B will also be eligible for automatic garbage collection at the same time as A. So there’s no need for a finalizer (nor would it be possible to write one) to explicitly deallocate object B.

2. Garbage collection and finalization

The end of an object’s life cycle in Java generally looks as follows:

  • at some point, the JVM determines that the object is no longer reachable: that is, there are no more references to it, or at least, no more references that will ever be accessed; at this point, the object is deemed eligible for garbage collection;
  • if the object has a finalizer, then it is scheduled for finalization;
  • if it has one, then at some later point in time, the finalizer may be called by some arbitrary thread allocated by the JVM for this purpose;
  • on its “next pass”, the garbage collector will check that the finalized object is still unreachable;
  • then at some arbitrary point in time in the future— but always after any finalization — the memory occupied by the object may actually deallocated.

You’ll notice that there are a few “mays” and “arbitraries” in this description. In particular, there are a few implications that we need to be aware of if we use a finalizer:

  • the finalizer may never actually get called, even if the object becomes unreachable;
  • having a finalizer adds some extra steps to an object’s life cycle so that in general, finalizers delay object deallocation;
  • because the object needs to be accessed later, finalizers prevent optimisations that can be made to allocation/deallocation: for example, our notion that the JVM can put an object on the stack goes out the window if the object will later need to be accessed by a separate finalizer thread at some arbitrary point in time.

So finalizers should really only be considered an “emergency” cleanup operation used on exceptional objects. A typical example is something like file stream or network socket. Although they provide a close() method, and we should generally try to use it, in the worst case, it’s better than nothing to have the stream closed at some future point when the stream object goes out of scope. And we know that in the very worst case, the stream or socket will be closed when our application exits— at least on sensible operating systems. This last point is important: the finalizer may not get called at all even in “normal” operation, so it’s really no good putting an operation in a finalizer whose execution is crucial to the system behaving once the application exits.

Another typical use of a finalizer is for objects linked to some native code that allocates resources outside the JVM. The JVM can’t automatically clean up or garbage collect such resources.

3. How to create a finalizer

So despite all the warnings, if you still want to create a finalizer, then essantially you need to override the finalize() method of the given class. (Strictly speaking, every class otherwise inherits an empty finalize() method from Object, but good VMs can “optimise away” the default empty finalizer.) A typical finalizer looks as follows:

protected void finalize() throws Throwable {
  super.finalize();
  ... cleanup code, e.g. deleting a temporary file,
      calling a native deallocate() method etc ...
}

General points about coding finalizers:

  • you should always call the superclass finalize() in case it needs to do some aditional cleanup;
  • you should always synchronize on shared resources, as finalizers can be run concurrently;
  • you should make no expectations about ordering of finalizers: if two objects are garbage collectable, there is no fixed order in which their finalizers will be run (and they could be run concurrently);
  • finalizers should run quickly, as they could otherwise delay the finalization (and hence garbage collection) of other objects;
  • you should not rely on seeing any exception thrown by a finalizer — the JVM will generally swallow them (but they will still interrupt the finalization of the object in question, of course).

4. Alternatives to finalizers

In general, it’s better to use an alternative to finalize() methods where possible— they really are a last resort. Safer alternatives include:

  • for objects with a well-defined lifetime, use an explicit close or cleanup method;
  • where useful, call the cleanup method in a finally block;
  • using a shutdown hook (see Runtime.addShutdownHook()) to perform actions such as deleting temporary files that just need to be done “when the application exits”.

And of course, don’t expect miracles. If the VM segfaults or there’s a power cut, there’s no cleanup method or shutdown hook that’s going to help you. If you are writing some critical application that requires “no data loss”, you will have to take other steps (such as using a before-and-after transaction log) to minimise potential problems caused by abnormal shutdown.