Recently I have faced twice the issue where JVM crashed due to OOM, and in both cases the root culprit was either directly the java.lang.ref.Finalizer Objects or one of the other Objects which was mysteriously not being GC.
In one case, we were trying to use a deprecated SUN API (sun.net.ftp.FtpClient) to get files from an FTP based on AS/400 machine. Our Server (Weblogic) used to start decently as:
Analyzing the Heap Dump we found that the main suspect was sun.net.ftp.FtpClient objects, which were being clogged in the Memory:
That led us to the magic world of Java Finalization!
What is Java Finalization?
Its a handle provided by JVM to developers to cleanup their non-heap, non-time-critical resources before the object containing references to these resources can be Garbage Collected.
How to use it?
Developer needs to override the protected void finalize() method in his API Class. By default empty implementation of finalize() is provided by java.lang.Object.
Whats the Catch?
Finalization seems to be a very insidious feature provided by JVM. If a developer decides to override finalize(), he might get into a trap where the Objects would stop (slow down) getting GC and eventually it would lead to OOM. How? read below:
A normal Object's (one using default finalize() from java.lang.Object) life cycle is something as:
Sequence of steps:
- An Object A, is created in memory and is referenced by one or more references.
- All the references pointing to the Object go out of scope, hence there is no reference pointing to the Object.
- GC runs and detects the Object A is collectible.
- Object is collected and memory is freed.
Whereas finalizable Object's (one having overridden finalize()) Life cycle would be:
Sequence of steps:
- An Object B, is created in memory and is referenced by one or more references. JVM detects that B is a finalizable Object and hence creates a java.lang.ref.Finalizer Object, having a reference to Object B. This Finalizer object is retained by the java.lang.ref.Finalize Class.
- All the references (other than the Finalizer) pointing to the Object go out of scope, hence there is no reference(but one) pointing to the Object B.
- When GC runs, it detects that Object B is being referenced only by the Finalizer. GC adds the Finalizer into the Queue - java.lang.ref.Finalizer.ReferenceQueue.
- Finalizer Thread, which is a Daemon thread, runs in a loop and one at a time, dequeue the Finalizers references from the Queue - java.lang.ref.Finalizer.ReferenceQueue.
- Finalizer Thread then invokes the finalize() method of the referent Object of the Finalizer, i.e. Object B's finalize() is invoked.
- Once the method call is completed, the Finalizer reference is removed from the java.lang.ref.Finalizer Class.
- GC runs and detects that the Finalzer Object is collectible, hence the referent Object B is collectible too.
- Object is collected and the memory is freed.
As you can see, a finalizable object needs at least two GCs to get collected. Also a buggy finalize() method will create a JVM level issue as Finalizer Thread would spend lot of time in the invocation, keeping many objects waiting in the queue to get their turn. This whole scenario can lead to JVM OOM error (that's why JVM doesn't guarantees that the finalize() of all finalizable objects will be called).
Hence try avoiding the finalization unless you really need it.
To know more on Finalization in detail and some alternative, read this - How to Handle Java Finalization's Memory-Retention Issues [By Tony Printezis].
Another great article on Finalization and its side effect - Debugging to understand Finalizers [By Nikita Salnikov-Tarnovski]Thanks for the read!!