Canceling ScheduledFutures (Memory Leak) / by Your Name

This is something I discovered very recently when I was trying to find a long-standing memory leak in our Application Servers. There were a couple of problems finding it. For one it started to become apparent only after 8-10 days and for second our servers already used vast amounts of RAM. We can't run Profilers like YourKit in our production environment and the gigantic 10GB+ HPROF memory dumps turned out to be not very useful (it's difficult to find something if there are millions of Objects).

In short, ScheduledFuture.cancel() or Future.cancel() in general does not notify its Executor that it has been cancelled and it stays in the Queue until its time for execution has arrived. It's not a big deal for simple Futures but can be a big problem for ScheduledFutures. It can stay there for seconds, minutes, hours, days, weeks, years or almost indefinitely depending on the delays it has been scheduled with.

Here is an example with the worst case scenario. The runnable and everything it's referencing will stay in the Queue for Long.MAX_VALUE milliseconds even after its Future has been cancelled!

public static void main(String[] args) {
    ScheduledThreadPoolExecutor executor 
        = new ScheduledThreadPoolExecutor(1);
    
    Runnable task = new Runnable() {
        @Override
        public void run() {
            System.out.println("Hello World!");
        }
    };
    
    ScheduledFuture future 
        = executor.schedule(task, 
            Long.MAX_VALUE, TimeUnit.MILLISECONDS);
    
    future.cancel(true);
}

You can see this by using a Profiler or calling the ScheduledThreadPoolExecutor.shutdownNow() method which will return a List with one element in it (it's the Runnable that got cancelled).

The solution for this problem is to either write your own Future implementation or to call the purge() method every now and then. Luckly we've been always using a custom Executor factory at work and it was just a matter of doing something simple as:

public static ScheduledThreadPoolExecutor createSingleScheduledExecutor() {
    final ScheduledThreadPoolExecutor executor 
        = new ScheduledThreadPoolExecutor(1);
    
    Runnable task = new Runnable() {
        @Override
        public void run() {
            executor.purge();
        }
    };
    
    executor.scheduleWithFixedDelay(task, 30L, 30L, TimeUnit.SECONDS);
    
    return executor;
}