Thursday, March 2, 2017

Java's Finalizer is Still There

When I was first learning Java and transitioning from C++ to Java, I remember being told repeatedly and frequently reading that one should not treat the Java finalizer like C++ destructors and should not count on it. The frequency and insistent nature of this advice had such effect on me that I cannot recall the last time I wrote a finalize() method and I cannot recall ever having written one in all the years I've written, read, reviewed, maintained, modified, and debugged Java code. Until recently, however, the effects of finalize() were not something I thought much about, probably because I have not used finalize(). A recent experience with finalize() has moved the effects of Java finalizers from an "academic exercise" to a real issue "in the wild."

The method-level Javadoc document comment for Object.finalize() provides some interesting details on the Java finalizer. It begins by providing an overall description of the method, "Called by the garbage collector on an object when garbage collection determines that there are no more references to the object. A subclass overrides the finalize method to dispose of system resources or to perform other cleanup." Another portion of this Javadoc comment warns of a couple issues commonly associated with use of Java finalizers: "The Java programming language does not guarantee which thread will invoke the finalize method for any given object. It is guaranteed, however, that the thread that invokes finalize will not be holding any user-visible synchronization locks when finalize is invoked. If an uncaught exception is thrown by the finalize method, the exception is ignored and finalization of that object terminates."

Josh Bloch devotes an item in Effective Java to the subject of Java finalizers. Item 7 of Effective Java's Second Edition is titled simply and concisely, "Avoid finalizers." Although many of the items in Effective Java use verbs such as "Prefer" or "Consider," this item uses the stronger verb "Avoid." Bloch does outline some examples where finalizers might be used, but his description of the inherent issues that remain and the many things to consider to mitigate those issues persuade most of us to avoid them as much as possible.

Bloch starts Effective Java item "Avoid Finalizers" with the emphasized (in bold) statement, "Finalizers are unpredictable, often dangerous, and generally unnecessary." Bloch emphasizes that developers should "never do anything time-critical in a finalizer" because "there is no guarantee [Java finalizers will] be executed promptly" and he emphasizes that developers should "never depend on a finalizer to update critical persistent state" because there is "no guarantee that [Java finalizers will] get executed at all." Bloch cites that exceptions in finalizers are not caught and warns of the danger of this because "uncaught exceptions can leave objects in a corrupt state."

The negative effect of Java finalizers that I had recent experience with is also described by Bloch. His "Avoid finalizers" item emphasizes (in bold), "there is a severe performance penalty for using finalizers" because it takes considerably longer "to create and destroy objects with finalizers." In our case, we were using a third-party library that internally used Java class finalize() methods to deallocate native memory (C/C++ through JNI). Because there was a very large number of these objects of these classes with finalize() methods, it appears that the system thread that handles Java finalization was getting behind and was locking on objects it was finalizing.

Garbage collection was also impacted adversely with the collector kicking off more frequently than we'd normally see. We realized quickly that the garbage collection logs were indicating garbage collection issues that were not easily traceable to typical heap size issues or memory leaks of our own classes. Running the highly useful jcmd against the JVM process with jcmd <pid> GC.class_histogram helped us to see the underlying culprit quickly. That class histogram showed enough instances of java.lang.ref.Finalizer to warrant it being listed third from the top. Because that class is typically quite a bit further down the class histogram, I don't even typically see it or think about it. When we realized that three more of the the top eight instances depicted in the class histogram were three classes from the third-party library and they they implemented finalize() methods, we were able to explain the behavior and lay blame on the finalizers (four of the top eight classes in the histogram made it a pretty safe accusation).

The Java Language Specification provides several details related to Java finalizers in Section 12.6 ("Finalization of Class Instances"). The section begins by describing Java finalizers: "The particular definition of finalize() that can be invoked for an object is called the finalizer of that object. Before the storage for an object is reclaimed by the garbage collector, the Java Virtual Machine will invoke the finalizer of that object." Some of the intentionally indeterminate characteristics of Java finalizers described in this section of the Java Language Specification are quoted here (I have added any emphasis):

  • "The Java programming language does not specify how soon a finalizer will be invoked."
  • "The Java programming language does not specify which thread will invoke the finalizer for any given object."
  • "Finalizers may be called in any order, or even concurrently."
  • "If an uncaught exception is thrown during the finalization, the exception is ignored and finalization of that object terminates."

I found myself enjoying working with the team that resolved this issue because I was able to experience in "real life" what I had only read about and knew about in an "academic" sense. It is always satisfying to apply a favorite tool (such as jcmd) and to apply previous experiences (such as recognizing what looked out of place in the jcmd class histogram) to resolve a new issue.

5 comments:

@DustinMarx said...

Stuart Marks has written a detailed post ["Deprecation of Object.finalize()"] on the background of Object.finalize(), why it's being deprecated in JDK 9 after so many years of Java developers being warned to not use it, and what it will eventually take to remove it completely from the JDK.

@DustinMarx said...

A post on the core-libs-dev JDK mailing list called "[11] RFR JDK-8198249: Remove deprecated Runtime::runFinalizersOnExit and System::runFinalizersOnExit" states, "Runtime.runFinalizersOnExit has been deprecated since 1.2 (1998) and deprecated for removal in JDK 9. ... I propose to remove Runtime.runFinalizersOnExit and System.runFinalizersOnExit methods in JDK 11." The JDK-8198249 issue states, "Runtime::runFinalizersOnExit is inherently unsafe and has been deprecated since 1.2. It has been deprecated for removal in Java SE 9."

@DustinMarx said...

The message "RFR 8212129: Remove finalize methods from java.util.zip.ZipFIle/Inflator/Deflator" on the core-libs-dev mailing list announces a "change [that] removes the finalize methods from java.util.zip.ZipFile/Inflator/Deflator."

Hamzah said...

So how did your team "resolve the issue"?
I see the same issue and have a hard time finding all finalizable objects in my heap. (all library code, not mine)

@DustinMarx said...

Hello Hamzah,

In our case, we worked with the provider of the library that used finalizers, demonstrated the negative consequences of so many finalizers, and provided ideas for alternatives. They implemented those alternatives quickly and the problem went away. If the presence of the heavy finalizer use is in a library over which you have no control, there seems to be little you can do other than to attempt to get that provider to change their implementation or to use an alternative to that library.

Dustin