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);
}
}
}
I think you didn't quite understood how to use Phantom References.
So in this line you should get NullPointerException:
Look at this line:
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 intoengineRefQueue
. In order to test it, you need to make sure that you are loosing this reference, and engine will be technically reachable only viaPhantomReference
.