Reference Queue is Always Empty

249 Views Asked by At

I am trying to clean a native resource when it is not accessible anymore. That resource provides a method to clean allocated resources (memory, threads etc.). To achieve this, I used Phantom Reference.

That resource should be created asynchronously when a new configuration provided by library user.

The problem is, ReferenceQueue is always empty. I don't reference the native resource outside of the files. Even in this situation, poll() method returns null. So I can't clean the resource and it causes to memory leak. How can I avoid this situation?

You can find the example code below. I used JDK 8.

// Engine.java

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Native source
 */
public class Engine {
    private static final Map<Long, byte[]> nativeResource = new HashMap<>();
    private static final AtomicLong counter = new AtomicLong(0);

    private final long id;

    public Engine() {
        // Simple memory leak implementation
        id = counter.incrementAndGet();
        nativeResource.put(id, new byte[1024 * 1024 * 10]);
    }

    public void close() {
        nativeResource.remove(id);
        System.out.println("Engine destroyed.");
    }
}
// EngineHolder.java

/**
 * Native source wrapper.
 */
public class EngineHolder {
    private final Engine engine;
    private final String version;

    EngineHolder(Engine engine, String version) {
        this.engine = engine;
        this.version = version;
    }

    public Engine getEngine() {
        return engine;
    }

    public String getVersion() {
        return version;
    }
}
import java.util.UUID;

// EngineConfiguration.java

/**
 * Native source configuration.
 */
public class EngineConfiguration {
    private final String version;

    public EngineConfiguration() {
        // Assign a new version number for configuration.
        this.version = UUID.randomUUID().toString();
    }

    public String getVersion() {
        return version;
    }
}
// SecuredRunnable.java

public class SecuredRunnable implements Runnable {
    private final Runnable runnable;

    public SecuredRunnable(Runnable runnable) {
        this.runnable = runnable;
    }

    @Override
    public void run() {
        try {
            this.runnable.run();
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

// EngineService.java

public class EngineService {
    private static EngineService INSTANCE = null;
    private static final Object INSTANCE_LOCK = new Object();

    private final ReferenceQueue<Engine> engineRefQueue = new ReferenceQueue<>();
    private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

    private volatile EngineConfiguration engineConfiguration;

    private volatile EngineHolder engineHolder;

    private EngineService() {
        engineConfiguration = new EngineConfiguration();

        EngineRunnable backgroundDaemon = new EngineRunnable();
        executor.scheduleWithFixedDelay(new SecuredRunnable(backgroundDaemon), 0, 5, TimeUnit.SECONDS);
    }

    public Engine getEngine() {
        return engineHolder != null ? engineHolder.getEngine() : null;
    }

    public void setEngineConfiguration(EngineConfiguration configuration) {
        this.engineConfiguration = configuration;
        // Dispatch job.
        EngineRunnable backgroundDaemon = new EngineRunnable();
        executor.submit(new SecuredRunnable(backgroundDaemon));
    }

    public static EngineService getInstance() {
        synchronized (INSTANCE_LOCK) {
            if (INSTANCE == null) {
                INSTANCE = new EngineService();
            }

            return INSTANCE;
        }
    }

    private static class EngineRunnable implements Runnable {
        @Override
        public void run() {
            EngineHolder engineHolder = INSTANCE.engineHolder;
            EngineConfiguration engineConfiguration = INSTANCE.engineConfiguration;

            // If there is no created engine or the previous engine is outdated, create a new engine.
            if (engineHolder == null || !engineHolder.getVersion().equals(engineConfiguration.getVersion())) {
                Engine engine = new Engine();
                INSTANCE.engineHolder = new EngineHolder(engine, engineConfiguration.getVersion());

                new PhantomReference<>(engine, INSTANCE.engineRefQueue);
                System.out.println("Engine created for version " + engineConfiguration.getVersion());
            }

            Reference<? extends Engine> referenceFromQueue;

            // Clean inaccessible native resources.
            while ((referenceFromQueue = INSTANCE.engineRefQueue.poll()) != null) {
                // This block doesn't work at all.
                System.out.println("Engine will be destroyed.");
                referenceFromQueue.get().close();
                referenceFromQueue.clear();
            }
        }
    }
}
// Application.java

public class Application {
    public static void main(String[] args) throws InterruptedException {
        EngineService engineService = EngineService.getInstance();

        while (true) {
            System.gc();
            engineService.setEngineConfiguration(new EngineConfiguration());
            Thread.sleep(100);
        }
    }
}
2

There are 2 best solutions below

5
On

I think you didn't quite understood how to use Phantom References.

  1. From Phantom Reference javadoc:

The get method of a phantom reference always returns null.

So in this line you should get NullPointerException:

referenceFromQueue.get().close();
  1. Look at this line:

    INSTANCE.engineHolder = new EngineHolder(engine, engineConfiguration.getVersion());
    

Reference to engine is always reachable (from static class) and this means it will never be collected by GC. And this means that your phantom reference will never be enqueued into engineRefQueue. In order to test it, you need to make sure that you are loosing this reference, and engine will be technically reachable only via PhantomReference.

If the garbage collector determines at a certain point in time that the referent of a phantom reference is phantom reachable, then at that time or at some later time it will enqueue the reference.

0
On

From the java.lang.ref package documentation:

The relationship between a registered reference object and its queue is one-sided. That is, a queue does not keep track of the references that are registered with it. If a registered reference becomes unreachable itself, then it will never be enqueued. It is the responsibility of the program using reference objects to ensure that the objects remain reachable for as long as the program is interested in their referents.

This is directly violated by your program by doing

new PhantomReference<>(engine, INSTANCE.engineRefQueue);

without keeping a reference to the PhantomReference. Therefore, as the queue doesn’t maintain a reference to the PhantomReference either, the PhantomReference itself is unreachable and simply gets garbage collected.