Automatic memory management (garbage collection) is one of essential aspects of Java platform. Garbage collection relieves developers from pain of memory management and protects them from whole range of memory related issues. Though, working with external resources (e.g. files and socket) from Java becomes tricky, because garbage collector alone is not enough to manage such resources.
Originally Java had finalizers facility. Later special reference classes were added to deal with same problem.
If we have some external resource which should be deallocated explicitly (common case with native libraries), this task could be solved either using finalizer or phantom reference. What is the difference?
Finalizer approach
Code below is implementing resource housekeeping using Java finalizer.
public class Resource implements ResourceFacade {
public static AtomicLong GLOBAL_ALLOCATED = new AtomicLong();
public static AtomicLong GLOBAL_RELEASED = new AtomicLong();
int[] data = new int[1 << 10];
protected boolean disposed;
public Resource() {
GLOBAL_ALLOCATED.incrementAndGet();
}
public synchronized void dispose() {
if (!disposed) {
disposed = true;
releaseResources();
}
}
protected void releaseResources() {
GLOBAL_RELEASED.incrementAndGet();
}
}
public class FinalizerHandle extends Resource {
protected void finalize() {
dispose();
}
}
public class FinalizedResourceFactory {
public static ResourceFacade newResource() {
return new FinalizerHandle();
}
}
Phantom reference approach
public class PhantomHandle implements ResourceFacade {
private final Resource resource;
public PhantomHandle(Resource resource) {
this.resource = resource;
}
public void dispose() {
resource.dispose();
}
Resource getResource() {
return resource;
}
}
public class PhantomResourceRef extends PhantomReference<PhantomHandle> {
private Resource resource;
public PhantomResourceRef(PhantomHandle referent, ReferenceQueue<? super PhantomHandle> q) {
super(referent, q);
this.resource = referent.getResource();
}
public void dispose() {
Resource r = resource;
if (r != null) {
r.dispose();
}
}
}
public class PhantomResourceFactory {
private static Set<Resource> GLOBAL_RESOURCES = Collections.synchronizedSet(new HashSet<Resource>());
private static ResourceDisposalQueue REF_QUEUE = new ResourceDisposalQueue();
private static ResourceDisposalThread REF_THREAD = new ResourceDisposalThread(REF_QUEUE);
public static ResourceFacade newResource() {
ReferedResource resource = new ReferedResource();
GLOBAL_RESOURCES.add(resource);
PhantomHandle handle = new PhantomHandle(resource);
PhantomResourceRef ref = new PhantomResourceRef(handle, REF_QUEUE);
resource.setPhantomReference(ref);
return handle;
}
private static class ReferedResource extends Resource {
@SuppressWarnings("unused")
private PhantomResourceRef handle;
void setPhantomReference(PhantomResourceRef ref) {
this.handle = ref;
}
@Override
public synchronized void dispose() {
handle = null;
GLOBAL_RESOURCES.remove(this);
super.dispose();
}
}
private static class ResourceDisposalQueue extends ReferenceQueue<PhantomHandle> {
}
private static class ResourceDisposalThread extends Thread {
private ResourceDisposalQueue queue;
public ResourceDisposalThread(ResourceDisposalQueue queue) {
this.queue = queue;
setDaemon(true);
setName("ReferenceDisposalThread");
start();
}
@Override
public void run() {
while(true) {
try {
PhantomResourceRef ref = (PhantomResourceRef) queue.remove();
ref.dispose();
ref.clear();
} catch (InterruptedException e) {
// ignore
}
}
}
}
}
Implementing same task using phantom reference requires more boilerplate. We need separate thread to handle reference queue, in addition, we need to keep strong references to allocated reference objects.
How finilaizers work in Java
Under the hood, finilizers work very similarly to our phantom reference implementation, though, JVM is hiding boilerplate from us.
Each time instance of object with finalizer is created, JVM creates instance of FinalReference class to track it. Once object becomes unreachable, FinalReference is triggered and added to global final reference queue, which is being processed by system finalizer thread.
So finalizes and phantom reference approach work very similar. Why should you bother with phantom references?
Comparing GC impact
Let's have simple test: resource object is allocated then added to the queue, once queue size hits limit oldest reference is evicted and thrown away. For this test we will monitor reference processing via GC logs.
Running finalizer based implementation.
[GC [ParNew[ ... [FinalReference, 5718 refs, 0.0063374 secs] ...
Released: 6937 In use: 59498
Running phantom based implementation.
[GC [ParNew[ ... [PhantomReference, 5532 refs, 0.0037622 secs] ...
Released: 5468 In use: 38897
As you can see, once object becomes unreachable, it needs to be handled in GC reference processing phase. Reference processing is a part of Stop-the-World pause. If, between collections, too many references becomes eligible for processing it may prolong Stop-the-World pause significantly.
In case above, there is no much difference between finalizers and phantom references. But let's change workflow a little. Now we would explicitly dispose 99% of handles and rely on GC only for 1% of references (i.e. semiautomatic resource management).
Running finalizer based implementation.
[GC [ParNew[ ... [FinalReference, 6295 refs, 0.0070033 secs] ...
Released: 6707 In use: 1457
Running phantom based implementation.
[GC [ParNew[ ... [PhantomReference, 625 refs, 0.0001551 secs] ...
Released: 21682 In use: 1217
For finalizer based implementation there is no difference. Explicit resource disposal doesn't help reduce GC overhead. But with phantoms, we can see what GC do not need to handle explicitly disposed references (so number of references process by GC is reduced by order of magnitude).
Why this is happening? When resource handle is disposed we drop reference to phantom reference object. Once phantom reference is unreachable, it would never be queued for processing by GC, thus saving time in reference processing phase. It is quite opposite with final references, once created it will be strong referenced by JVM until being processed by finalizer thread.
Conclusion
Using phantom references for resources housekeeping requires more work compared to plain finalizer approach. But using phantom references you have far more granular control over whole process and implement number of optimizations such as hybrid (manual + automatic) resource management.
Full source code used for this article is available at https://github.com/aragozin/example-finalization.
so clear.thanks
ReplyDelete