the informal ramblings of a formal language researcher

Monday, April 11, 2005

Observing Finalizers and References on the JVM

As a followup to clarify my previous comment on this post (since Dave and I discussed this question personally):

Here is an experimental revision of the code that Dave posted; it is meant to demonstrate a number of things. The most important are:

  • If you want to really observe a space leak, it is helpful to actually use objects which are large (new int[X] is your friend here).
  • If you want to really observe a space leak, it (unfortunately) seems like you may have to employ the (non-standard) -Xms and -Xmx options to the JVM.
  • There are more reliable/sensible alternatives for monitoring whether an object has been collected than attempting to maintain a hashtable of object identifiers. Namely, use of classes in java.lang.ref.*.

    • Having said this, I admit that I do not fully understand the semantics of weak references, because I was not able to naively replace all of Dave's uses of kill(int) with my killAlt(int) procedure.

  • Finalizers really are run only once. However, that doesn't always mean that you can't collect an object just because the call to its finalizer makes it live again.


I had to backport the code to JDK 1.4 since that's all I have on my poor little Mac OS X box. Sorry. I tried to make the backport clean by using trampoline instantiation of the generic collections rather than cluttering up the code with casts everywhere.

Sample command line invocations of this class:

% java -Xmx1mb -Xms1mb Immortal -dummysize 100000 -num 10

% java -Xmx1mb -Xms1mb Immortal -dummysize 100000 -blowup 10

(Note that due to the braindead way I'm doing argument parsing, the order of the options above is VERY significant).


import java.util.Hashtable;
import java.util.Vector;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.ref.ReferenceQueue;

public class Immortal {

static class HashtableFromIntegerToInteger {
Hashtable me = new Hashtable();
public int get(int k) { return ((Integer)me.get(new Integer(k))).intValue(); }
public void put(int k, int v) { me.put(new Integer(k), new Integer(v)); }
public void remove(int k) { me.remove(new Integer(k)); }
}

static class HashtableFromIntegerToImmortal {
Hashtable me = new Hashtable();
public Immortal get(int k) { return ((Immortal)me.get(new Integer(k))); }
public void put(int k, Immortal v) { me.put(new Integer(k), v); }
public void remove(int k) { me.remove(new Integer(k)); }
}

static class HashtableFromIntegerToRef {
Hashtable me = new Hashtable();
public Reference get(int k) { return ((Reference)me.get(new Integer(k))); }
public void put(int k, Reference v) { me.put(new Integer(k), v); }
public void remove(int k) { me.remove(new Integer(k)); }
}

/** tracks the number of times each finalizer is run */
// private static Hashtable finalizeCounts = new Hashtable();
private static HashtableFromIntegerToInteger finalizeCounts = new HashtableFromIntegerToInteger();

/** used to control object lifetimes */
private static HashtableFromIntegerToImmortal pointers = new HashtableFromIntegerToImmortal();

private static HashtableFromIntegerToRef phantoms = new HashtableFromIntegerToRef();

private static ReferenceQueue refqueue = new ReferenceQueue();

private static boolean nomesg = false;

private static void mesg(String s) {
if (! nomesg) {
System.out.println(s);
}
}

/** used to generate unique identifier codes */
private static int unique = 0;

/** some "large" state so that we can observe blowup */
public int[] dummydata;
private static int dummysize = 100;

public static void killAlt(int id) {
int finalizeCount = finalizeCounts.get(id);
pointers.remove(id);
while ( ! phantoms.get(id).isEnqueued())
{
mesg("(alt) trying to kill " + id + "...");
System.gc();
}
}

/** deletes the object with the given id. */
public static void kill(int id)
{
int finalizeCount = finalizeCounts.get(id);
pointers.remove(id);
while (finalizeCounts.get(id) == finalizeCount)
{
mesg("trying to kill " + id + "...");
System.gc();
}
}

// The code from these two methods can't be inlined, because
// we rely on their stack frame disappearing to prevent the
// link to the tracked object from persisting.

public static int makeTemporaryObject()
{
Immortal temp = new Immortal();
return temp.id;
}

public static void doSomethingWith(int id)
{
Immortal temp = pointers.get(id);
temp.sayHello();
}

/** identifier code */
private int id;

private Immortal()
{
id = unique++;
mesg("creating instance " + id);
finalizeCounts.put(id, 0);
pointers.put(id, this);
phantoms.put(id, new WeakReference(this, refqueue));
int last = dummysize;
dummydata = new int[last + 1];
dummydata[0] = 1;
dummydata[last] = 1;
}

public void sayHello()
{
System.out.println("hi, I'm number " + id);
}

public void finalize()
{
mesg("finalizing " + id + "...");
finalizeCounts.put(id, finalizeCounts.get(id) + 1);
// clear! *khzh-thump*
pointers.put(id, this);
}

public static void main(String[] args) {

if (args.length == 0) {
int id = Immortal.makeTemporaryObject();

// This causes the finalizer to run (in the GC thread.)
Immortal.kill(id);

// And yet, the object is still alive!
Immortal.doSomethingWith(id);

// This will now loop infinitely, since the finalizer
// will never be run a second time.
Immortal.kill(id);
} else {
for(int i = 0; i < args.length; i++) {
if (false) {
// dummy case for uniform syntax
} else if (args[i].equals("-nomesg")) {
nomesg = true;
} else if (args[i].equals("-dummysize")) {
int num = Integer.parseInt(args[++i]);
dummysize = num;
} else if (args[i].equals("-num")) {
int num = Integer.parseInt(args[++i]);

for(int j = 0; j < num; j++) {
int id = Immortal.makeTemporaryObject();

// This causes the finalizer to run (in the GC thread.)
Immortal.kill(id);

// And yet, the object is still alive!
Immortal.doSomethingWith(id);

// FSK: alternative variant of kill which uses weak references
Immortal.killAlt(id);
}
} else if (args[i].equals("-blowup")) {
int num = Integer.parseInt(args[++i]);

for(int j = 0; j < num; j++) {
int id = Immortal.makeTemporaryObject();

// This causes the finalizer to run (in the GC thread.)
Immortal.kill(id);

// And yet, the object is still alive!
Immortal.doSomethingWith(id);

}
}
}
}
}
}

No comments:

Followers