A colleague pointed my to Dr Heinz M. Kabutz's article on Java finalizers. The article compares the performance of objects that have a trivial (empty) finalizer vs those that have a very simple finalizer. The basic outcome is that the running time for the non-trivial case is orders of magnitude larger than the trivial case.
I was quite shocked when I saw this since the simple finalizer given in the example is similar to what is proposed in .net IDisposable pattern. I was thinking "Holy Smoke, Batman! How can it really be that bad??". The answer is in Jack Shirazi's article: basically the finalizer causes the object to live through the nursery collection and thus puts much more pressure on the mature space collector.
.net?
I decided to implement the test in C# quickly and see how it performed. Here is the code:
using System;The most important change is the added call to "GC.SuppressFinalize". This test case is the specific reason for this call - basically it removes the object from the finalization queue thus allowing it to be collected in the nursery collection. I also added an initial loop to allow the JIT to compile the classes in use. Here are the results:
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1
{
public class ConditionalFinalizer
{
private static readonly bool DEBUG = false;
// Should be volatile as it is accessed from multiple threads.
// Thanks to Anton Muhin for pointing that out.
private volatile bool resourceClosed;
private readonly int id;
public ConditionalFinalizer(int id)
{
this.id = id;
resourceClosed = false;
}
~ConditionalFinalizer()
{
if (DEBUG)
{
if (!resourceClosed)
{
Console.Error.WriteLine("You forgot to close the resource with id " + id);
}
resourceClosed = true;
}
}
public void close()
{
resourceClosed = true;
GC.SuppressFinalize(this);
}
}
class Program
{
static void Main(string[] args)
{
// Allow the JIT to warm up
for (int i = 0; i < 10 * 1000 * 1000; i++)
{
ConditionalFinalizer cf = new ConditionalFinalizer(i);
if (i % (1000 * 1000) != 0)
{
cf.close();
}
}
DateTime start = DateTime.Now;
for (int i = 0; i < 10 * 1000 * 1000; i++)
{
ConditionalFinalizer cf = new ConditionalFinalizer(i);
if (i % (1000 * 1000) != 0)
{
cf.close();
}
}
TimeSpan time = DateTime.Now - start;
Console.WriteLine("time = " + time.TotalMilliseconds);
Console.ReadKey();
}
}
}
Calling "GC.SuppressFinalize" dramatically improves the performance for .net and my initial results line up with Dr Kabutz's. Java really does a neat optimisation for the trivial finalizer case! Unfortunately it also gets smashed in the non-trivial case...
Java
Dr Kabutz's code for showing the classes pending finalization goes 90% of the way towards providing something similar to "GC.SuppressFinalize", I decided to add that last little bit:
import java.lang.ref.Reference;
import java.lang.reflect.Field;
public class FinalizeHelper {
static FinalizeHelper finalizeHelper = new FinalizeHelper();
private final Class<?> finalizerClazz;
private final Object lock;
private final Field unfinalizedField;
private final Field nextField;
private final Field prevField;
private final Field referentField;
public FinalizeHelper() {
try {
finalizerClazz = Class.forName("java.lang.ref.Finalizer");
// we need to lock on this field to avoid racing conditions
Field lockField = finalizerClazz.getDeclaredField("lock");
lockField.setAccessible(true);
lock = lockField.get(null);
// the start into the linked list of finalizers
unfinalizedField = finalizerClazz.getDeclaredField("unfinalized");
unfinalizedField.setAccessible(true);
// the next element in the linked list
nextField = finalizerClazz.getDeclaredField("next");
nextField.setAccessible(true);
// the prev element in the linked list
prevField = finalizerClazz.getDeclaredField("prev");
prevField.setAccessible(true);
// the object that the finalizer is defined on
referentField = Reference.class.getDeclaredField("referent");
referentField.setAccessible(true);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException("Could not create FinalizeHelper", e);
}
}
private void suppress(Object instance) {
try {
synchronized (lock) {
// Get the start of the un-finalized list
Object current = unfinalizedField.get(null);
Object previous = null;
while (current != null) {
Object value = referentField.get(current);
// Check if this entry refers to the instance we are interested in
if (value == instance) {
// Unlink the current entry from the queue
Object next = nextField.get(current);
if (previous == null) {
unfinalizedField.set(null, next);
prevField.set(next, null);
} else {
nextField.set(previous, next);
prevField.set(next, previous);
}
break;
}
// Move to the next entry
previous = current;
current = nextField.get(current);
}
}
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
public static void suppressFinalize(Object instance) {
finalizeHelper.suppress(instance);
}
}
Here is the updated test files:
public class ConditionalFinalizer {
private static final boolean DEBUG = false;
// Should be volatile as it is accessed from multiple threads.
// Thanks to Anton Muhin for pointing that out.
private volatile boolean resourceClosed;
private final int id;
public ConditionalFinalizer(int id) {
this.id = id;
resourceClosed = false;
}
protected void finalize() throws Throwable {
if (DEBUG) {
if (!resourceClosed) {
System.err.println("You forgot to close the resource with id " + id);
}
resourceClosed = true;
super.finalize();
}
}
public void close() {
resourceClosed = true;
FinalizeHelper.suppressFinalize(this);
}
}
and:
public class ConditionalFinalizerTest1 {
public static void main(String[] args) throws InterruptedException {
long time = System.currentTimeMillis();
for (int i = 0; i < 10 * 1000 * 1000; i++) {
ConditionalFinalizer cf = new ConditionalFinalizer(i);
if (i % (1000 * 1000) != 0) {
cf.close();
}
}
time = System.currentTimeMillis() - time;
System.out.println("time = " + time);
}
}
And here are the updated results:
Suppressing the finalizer for those objects significantly increases the performance of the test. The difference in performance between .net and Java is probably because I'm using reflection to scan the finalizer list in the Java case.
Summary
There is a definite benefit to suppressing finalizers for object that get manually disposed, its just a pity that there is no native "GC.SuppressFinalize" method for Java. The IDiposable pattern is very useful for situations where you want to be able to:
- Manually dispose of an item when you know it is no longer needed.
- Be able to run clean up code to safely bring down connections, close files, etc.
- Be able to rely on the finalizer to catch situation where it is not known when an object should be cleaned up or to catch mistakes that mean the clean code is not explicitly called.
No comments:
Post a Comment