1

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);
        }
    }
}
Emre Turan
  • 101
  • 1
  • 9

2 Answers2

1

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.

Igor Konoplyanko
  • 9,176
  • 6
  • 57
  • 100
  • Actually I am aware of the first problem but the problem is, queue is always empty. So I expect a NullPointerException, you are right. I thought when EngineHolder object is not accessible, GC will clean the Engine object too because it is referenced in EngineHolder object. – Emre Turan Jun 01 '21 at 08:30
  • Your GC reachability looks like this now: static EngineService INSTANCE -> INSTANCE.EngineHolder -> Engine engine. Try to get rid of static references, as they will be always reachable by GC. Check out this answer: https://stackoverflow.com/questions/27186799/what-are-gc-roots-for-classes – Igor Konoplyanko Jun 01 '21 at 09:04
  • But in run method, I create a new Engine and EngineHolder, then I assign it to INSTANCE.engineHolder. So, the previous EngineHolder is inaccessible. That means, Engine in the previous EngineHolder is inaccessible too, right? GC should clean that branch. – Emre Turan Jun 01 '21 at 09:34
  • 1
    Yes, that's right, i have overlooked that. Try to make sure that PhantomReferences are really getting into the queue with `reference.isEnqueued()`? (need to refactor some code then) – Igor Konoplyanko Jun 01 '21 at 10:38
  • I will check it, but probably I will change my implementation. Probably I will implement my own garbage collection system for that native source. – Emre Turan Jun 02 '21 at 02:28
1

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.

Holger
  • 285,553
  • 42
  • 434
  • 765