Skip to content

Reclaim native resources with Cleaner

John Jiang edited this page Jan 30, 2022 · 4 revisions

Reclaim native resources

We know one of the biggest advantages in Java programming language is automatic memory management. JVM garbage collectors alleviate the pains on releasing resources when the associated objects are not needed anymore.

However, in some scenarios, objects are linked to native resources, say JNI memory. If only the objects themselves are reclaimed, the native resources may not be reclaimed accordingly. This problem possibly lead to the resources leak.

Finalizer

A traditional (or stereotyped) approach for releasing the resources is overriding the method Object::finalize. Finalizers invoke these overridden methods before the associated objects are being collected by garbage collectors. The below codes demonstrate how to release resources via finalization.

A Resource instance represents a native resource.

public class Resource {

    public final String name;

    public Resource(String name) {
        this.name = name;
    }
}

A ResourceHolder instance creates a native resource underlying. Before a ResourceHolder instance is GCed, the associated native resource should be released by Finalizer.

public class ResourceHolder {

    private final Resource resource;

    public ResourceHolder(String name) {
        this.resource = new Resource(name);
    }

    @Override
    protected void finalize() {
        // Represent the native resource is reclaimed by a Finalizer thread.
        System.out.println(Thread.currentThread() + " finalized " + resource.name);
    }
}

The following application takes advantage of Finalizer to reclaim the resources.

public class FinalizerUsage {

    public static void main(String[] args) throws Exception {
        // Create the resource holders, which create underlying
        // native resources automatically.
        for (int i = 0; i < 5; i++) {
            new ResourceHolder("resource-" + i);
        }

        // Explicitly call GC
        System.gc();

        TimeUnit.SECONDS.sleep(1);
    }
}

The output looks like the below,

Thread[Finalizer,8,system] finalized resource-4
Thread[Finalizer,8,system] finalized resource-0
Thread[Finalizer,8,system] finalized resource-1
Thread[Finalizer,8,system] finalized resource-2
Thread[Finalizer,8,system] finalized resource-3

It shows that a Finalizer thread called the finalize methods and released the resources respectively.

Problems on Finalizer

Although the finalization mechanism looks straightforward and working, it brings some known issues, see item #8 in Effective Java 3rd.

  • The resources may be run out before garbage collection happens.
  • The exceptions from finalization are ignored, and the finalization just terminates, and the states of objects are corrupted.
  • Finalizers open your class up to finalizer attacks.
  • Finalization is low performance.

In fact, Object::finalize method has been deprecated since JDK 9. Furthermore, JEP 421 delivered in JDK 18 already declared this method will be removed in a future release. The desired way is closing the resources immediately after they are no longer used.

Cleaner

In real world framework or libraries, there may not architectures for closing resources explicitly. For example, the Service Provider Interfaces defined by Java Cryptography Architecture(JCA) don't expose any closing method for reclaiming resources. So, JCA implementations cannot close any resource immediately. For such scenarios, an alternative feasible solution is using Cleaner, say the public java.lang.ref.Cleaner or the (unrecommended) internal jdk.internal.ref.Cleaner. Both of them use PhantomReference and ReferenceQueue to be notified when the associated instances are phantom reachable.

Similar to Finalizer, Cleaner also has some known issues, like low performance. Item #8 in Effective Java 3rd mentions those drawbacks. But Cleaner is much better, at least more robust, than Finalizer. The below codes demonstrate how to release resources via Cleaner. But this demonstration doesn't use JDK-builtin Cleaners. Instead, it develops a custom Cleaner, though PhantomReference is leveraged as well.

The previous Resource class is not changed, but this ResourceHolder doesn't override method Object::finalize.

public class ResourceHolder {

    public final Resource resource;

    public ResourceHolder(String name) {
        this.resource = new Resource(name);
    }
}

This custom Cleaner links objects (here resource holders) to a ReferenceQueue for phantom references.

public class Cleaner {

    private final Collection<ResourceHolderRef> resourceHolderRefs
            = new ConcurrentLinkedQueue<>();

    private final ReferenceQueue<Object> resourceHolderRefQueue
            = new ReferenceQueue<>();

    // Associate resource holder with phantom reference queue.
    private class ResourceHolderRef extends PhantomReference<Object> {

        private final CleanAction cleanAction;

        private ResourceHolderRef(Object resourceHolder,
                                  ReferenceQueue<Object> resourceHolderRefQueue,
                                  CleanAction cleanAction) {
            super(resourceHolder, resourceHolderRefQueue);
            this.cleanAction = cleanAction;
        }

        // When the resource holder is cleared, the associated cleaning action
        // is invoked and the underlying resource is reclaimed accordingly.
        @Override
        public void clear() {
            cleanAction.clean();
            resourceHolderRefs.remove(this);
            super.clear();
        }
    }

    private final Thread cleanerThread;
    private boolean started = false;

    public Cleaner() {
        cleanerThread = new Thread(() -> {
            while (true) {
                try {
                    // Remove a phantom reachable reference from the reference queue,
                    // and do the cleaning action associated to it.
                    resourceHolderRefQueue.remove().clear();
                } catch (Throwable t) {
                    // Do nothing
                }
            }
        });
        cleanerThread.setDaemon(true);
    }

    // Start the cleaning thread.
    public synchronized void start() {
        if (!started) {
            cleanerThread.start();
            started = true;
        }
    }

    // Register the resource holder and the associated cleaning action to the cleaner.
    public void register(Object resourceHolder, CleanAction cleanAction) {
        Objects.requireNonNull(resourceHolder);
        Objects.requireNonNull(cleanAction);

        resourceHolderRefs.add(new ResourceHolderRef(
                resourceHolder, resourceHolderRefQueue, cleanAction));
    }

    // The abstraction on cleaning.
    public interface CleanAction {

        void clean();
    }
}

The following application takes advantage of the above Cleaner to reclaim the resources.

public class CleanerUsage {

    private static class CleanResource implements Cleaner.CleanAction {

        private final Resource resource;

        private CleanResource(Resource resource) {
            this.resource = resource;
        }

        @Override
        public void clean() {
            System.out.println(Thread.currentThread() + " cleaned " + resource.name);
        }
    }

    public static void main(String[] args) throws Exception {
        // Create and start a cleaner.
        Cleaner cleaner = new Cleaner();
        cleaner.start();

        // Create the resource holders, which create underlying native
        // resources automatically. And register them and the associated
        // clean action to the cleaner.
        for (int i = 0; i < 5; i++) {
            ResourceHolder resourceHolder = new ResourceHolder("resource-" + i);
            cleaner.register(
                    resourceHolder,
                    new CleanResource(resourceHolder.resource));
        }

        // Explicitly call GC
        System.gc();

        // Wait for the cleaning.
        TimeUnit.SECONDS.sleep(1);
    }
}

The output looks like the below,

Thread[Thread-0,5,main] cleaned resource-4
Thread[Thread-0,5,main] cleaned resource-3
Thread[Thread-0,5,main] cleaned resource-2
Thread[Thread-0,5,main] cleaned resource-1
Thread[Thread-0,5,main] cleaned resource-0

It shows that a Cleaner thread called the clean methods and released the resources respectively. Generally, it prefers to only one Cleaner for an independent library. Namely, only one thread is used to release all native resources if possible.