Search code examples
javastaticgarbage-collectionsuperclasstimertask

Is an abstract superclass garbage collected when the last instance of the subclass is collected?


I have an abstract class which I use to expire instances of a subclass:

public abstract class Expirable {
    private transient Timer timer;

    protected abstract void onExpire();

    protected void setExpire(long delay) {
        resetExpire();

        timer = new Timer();
        timer.schedule(new TimerTask() {
                           @Override
                           public void run() {
                               resetExpire();
                               onExpire();
                           }
                       }, delay
        );
    }

    protected void resetExpire() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }
}

I extend any class and overide onExpire, then I call setExpire(delay) from the subclass (not shown):

public class MyClass extends Expirable {
    @Override
    protected void onExpire() {
        // expiration code here
    }
}

This class works perfectly fine, but the timer object is very expensive. Because I have tens of thousands of instances, I wrote a cheap version with the same functionality, which uses a single timer scheduled at a fixed rate and a queue. Not as precise, but cheap.

public abstract class ExpirableCheap {
    private static final long COLLECTOR_INTERVAL_MSEC = 1000;
    private static final int INITIAL_CAPACITY = 128;

    private static Queue<ExpirableCheap> queue = new PriorityBlockingQueue<>(
            INITIAL_CAPACITY,
            Comparator.comparingLong(expirable -> expirable.expiresWhen)
    );

    @SuppressWarnings("FieldCanBeLocal")
    private static TimerTask timerTask;
    @SuppressWarnings("FieldCanBeLocal")
    private static Timer timer;
    static {
        timerTask = new TimerTask() {
            @Override
            public void run() {
                // this Runnable stops working
                long time = new Date().getTime();
                while (queue.peek() != null && queue.peek().expiresWhen < time) {
                    queue.poll().onExpire();
                }
            }
        };
        timer = new Timer();
        timer.scheduleAtFixedRate(timerTask,
                COLLECTOR_INTERVAL_MSEC,
                COLLECTOR_INTERVAL_MSEC
        );
    }

    private transient long expiresWhen;

    protected abstract void onExpire();

    protected synchronized void setExpire(long delay) {
        resetExpire();

        long time = new Date().getTime();
        expiresWhen = time + delay;

        queue.offer(this);
    }

    protected synchronized void resetExpire() {
        queue.remove(this);
    }
}

Obviously, the static code block is executed once and schedules the timer in regular intervals. The timerTask peeks at the queue and calls onExpire().

What's wrong?

This runs fine for a while, but then suddenly the timerTask is no longer executed. When testing, it works fine and I cannot simulate the situation, but it fails after some time in production.

I am not sure what happens, but I suspect the static variables which I initialized in the static code block are garbage collected when the last instance of the subclass is collected. Then, when the class is re-used, the static code block is not run again. In other words, it seems to work until there are no instances anymore which extend ExpirableCheap.

Strangely, the queue persists, reason why I expected an exception to happen inside the Runnable, which I believe is not the case.

As you can see, I tried to move timer and timerTask variables from the static code block into member variables (which didn't help). I also tried to synchronize setExpire() and resetExpire(), which I believe makes no difference either.

Can anybody see what's happening? Did I make another stupid mistake and am I on the wrong track?

Any suggestions what I could change to make this work?


Solution

  • As @TimBiegeleisen correctly pointed out, Java works as expected. An abstract superclass is NOT garbage collected when the last instance of the subclass is collected.

    My problem was unrelated.