GC是Java平台的一个重要特性,大大减轻了开发人员对内存管理的痛苦,帮助他们不受内存相关的问题影响。然而,当在java代码中使用了外部资源(例如文件和套接字),内存管理将变得棘手,因为单独的GC不足以管理这样的资源。
假设我们有如下场景,我们需要在Resource实例被销毁前,调用到dispose()方法:
public interface ResourceFacade {
public void dispose();
}
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();
}
}
java提供了Finalizer方式,可以帮助开发者在gc时对资源进行最后的回收操作,对上述问题,可以这样解决:
public class FinalizerHandle extends Resource {
protected void finalize() {
dispose();
}
}
public class FinalizedResourceFactory {
public static ResourceFacade newResource() {
return new FinalizerHandle();
}
}
其中,FinalizerHandle类实现了Object类的finalize()方法,当FinalizerHandle被回收的时候,会调用finalize()方法,从而完成对resource资源的回收。
java还提供了虚引用的方式,帮助开发者解决上述问题:
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 {
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 GLOBAL_RESOURCES = Collections.synchronizedSet(new HashSet());
private static ResourceDisposalQueue REF_QUEUE = new ResourceDisposalQueue();
@SuppressWarnings("unused")
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.clear();
handle = null;
super.dispose();
GLOBAL_RESOURCES.remove(this);
}
}
private static class ResourceDisposalQueue extends ReferenceQueue {
}
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
}
}
}
}
}
其实,finilaizers和虚引用的工作方式非常相似,每次创建实现了finalize()方法的对象实例时,jvm队徽创建一个FinalReference类的实例来跟踪该对象,一旦该对象不可达,FinalReference将会将该对象添加到全局最终引用队列,这个队列的对象将会被系统finilaizer线程所处理。
这样看起来,finilaizer的工作方式和我们所实现的虚引用资源回收很类似,那么我们为何还要自己回收呢?
我们不断创建新生代对象和resource对象,来对比两个方式在gc时的消耗。
package reftest;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import org.junit.Test;
public class ReferenceUsageCheck {
public int queueLimit = 1000;
public Deque queue = new ArrayDeque();
@Test
public void leaking_references_finalizer() {
while(true) {
gcFiller();
reportStats();
ResourceFacade rf = FinalizedResourceFactory.newResource();
queue.addLast(rf);
if (queue.size() > queueLimit) {
queue.removeFirst();
}
}
}
@Test
public void leaking_references_phantom() {
while(true) {
gcFiller();
reportStats();
ResourceFacade rf = PhantomResourceFactory.newResource();
queue.addLast(rf);
if (queue.size() > queueLimit) {
queue.removeFirst();
}
}
}
GarbageCollectorMXBean youngGC = ManagementFactory.getGarbageCollectorMXBeans().get(0);
GarbageCollectorMXBean oldGC = ManagementFactory.getGarbageCollectorMXBeans().get(1);
long lastYoungGC;
long lastOldGC;
long lastReleased;
public void reportStats() {
if (lastYoungGC != youngGC.getCollectionCount() || lastOldGC != oldGC.getCollectionCount()) {
long released = Resource.GLOBAL_RELEASED.get() - lastReleased;
long used = Resource.GLOBAL_ALLOCATED.get() - Resource.GLOBAL_RELEASED.get();
lastReleased = Resource.GLOBAL_RELEASED.get();
System.out.println("Released: " + released + " In use: " + used);
lastYoungGC = youngGC.getCollectionCount();
lastOldGC = oldGC.getCollectionCount();
}
}
public void gcFiller() {
List data = new ArrayList();
for(int i = 0; i != 16; ++i) {
data.add(new int[256]);
}
data.toString();
}
}
添加jvm参数:-XX:+PrintGCDetails -XX:+PrintReferenceGC -Xloggc:gc.log
可以看到gc log中FinalReference和PhantomReference清理耗时对比。
[FinalReference, 41466 refs, 0.0601781 secs]
[FinalReference, 36873 refs, 0.0421081 secs]
[FinalReference, 36873 refs, 0.0877035 secs]
[FinalReference, 35257 refs, 0.0871575 secs]
[FinalReference, 35257 refs, 0.0422109 secs]
[FinalReference, 34959 refs, 0.0408274 secs]
[FinalReference, 34958 refs, 0.0455706 secs]
[FinalReference, 34981 refs, 0.0410302 secs]
[FinalReference, 34980 refs, 0.0439679 secs]
[PhantomReference, 36771 refs, 0 refs, 0.0340093 secs]
[PhantomReference, 36770 refs, 0 refs, 0.0251031 secs]
[PhantomReference, 35157 refs, 0 refs, 0.0141126 secs]
[PhantomReference, 35157 refs, 0 refs, 0.0447339 secs]
[PhantomReference, 34818 refs, 0 refs, 0.0114922 secs]
[PhantomReference, 34817 refs, 0 refs, 0.0134069 secs]
[PhantomReference, 34860 refs, 0 refs, 0.0228386 secs]
[PhantomReference, 34861 refs, 0 refs, 0.0147699 secs]
[PhantomReference, 35136 refs, 0 refs, 0.0266345 secs]
可以明显看到FinalReference耗用了更多时间,原因是在这个回收过程中,调用的resource的dispose()方法,这个方法的耗时被加入的gc时间中。而在PhantomReference方式中,PhantomReference直接可以被回收,我们将调用dispose()方法的过程放在我们自己的守护线程中,大大减少了reference回收过程中的耗时。
而reference清理过程在gc过程中是stw的,因此,我们可以认为,基于PhantomReference的资源回收,可以获得更好的性能。
如果我们手动进行资源回收,效果会如何:
package reftest;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import org.junit.Test;
public class ReferenceUsageCheck {
public int queueLimit = 1000;
public Deque queue = new ArrayDeque();
@Test
public void low_leaking_references_finalizer() {
int n = 0;
while(true) {
gcFiller();
reportStats();
ResourceFacade rf = FinalizedResourceFactory.newResource();
queue.addLast(rf);
if (queue.size() > queueLimit) {
rf = queue.removeFirst();
if ((++n) % 100 != 0) {
rf.dispose();
}
}
}
}
@Test
public void low_leaking_references_phantom() {
int n = 0;
while(true) {
gcFiller();
reportStats();
ResourceFacade rf = PhantomResourceFactory.newResource();
queue.addLast(rf);
if (queue.size() > queueLimit) {
rf = queue.removeFirst();
if ((++n) % 100 != 0) {
rf.dispose();
}
}
}
}
GarbageCollectorMXBean youngGC = ManagementFactory.getGarbageCollectorMXBeans().get(0);
GarbageCollectorMXBean oldGC = ManagementFactory.getGarbageCollectorMXBeans().get(1);
long lastYoungGC;
long lastOldGC;
long lastReleased;
public void reportStats() {
if (lastYoungGC != youngGC.getCollectionCount() || lastOldGC != oldGC.getCollectionCount()) {
long released = Resource.GLOBAL_RELEASED.get() - lastReleased;
long used = Resource.GLOBAL_ALLOCATED.get() - Resource.GLOBAL_RELEASED.get();
lastReleased = Resource.GLOBAL_RELEASED.get();
System.out.println("Released: " + released + " In use: " + used);
lastYoungGC = youngGC.getCollectionCount();
lastOldGC = oldGC.getCollectionCount();
}
}
public void gcFiller() {
List data = new ArrayList();
for(int i = 0; i != 16; ++i) {
data.add(new int[256]);
}
data.toString();
}
}
添加jvm参数:-XX:+PrintGCDetails -XX:+PrintReferenceGC -Xloggc:gc.log
可以看到gc log中FinalReference和PhantomReference清理耗时对比。
[FinalReference, 41679 refs, 0.1855998 secs]
[FinalReference, 41467 refs, 0.0959650 secs]
[FinalReference, 41466 refs, 0.0749919 secs]
[FinalReference, 36873 refs, 0.0556182 secs]
[FinalReference, 36874 refs, 0.0947935 secs]
[FinalReference, 35257 refs, 0.0422348 secs]
[FinalReference, 35256 refs, 0.0505925 secs]
[FinalReference, 34959 refs, 0.0749728 secs]
[FinalReference, 34959 refs, 0.0468931 secs]
[FinalReference, 34980 refs, 0.0435589 secs]
[FinalReference, 34980 refs, 0.0501272 secs]
[FinalReference, 35236 refs, 0.0396520 secs]
[PhantomReference, 1325 refs, 0 refs, 0.0001576 secs]
[PhantomReference, 1563 refs, 0 refs, 0.0001802 secs]
[PhantomReference, 1563 refs, 0 refs, 0.0001771 secs]
[PhantomReference, 1563 refs, 0 refs, 0.0002009 secs]
[PhantomReference, 1563 refs, 0 refs, 0.0001584 secs]
[PhantomReference, 1562 refs, 0 refs, 0.0001625 secs]
[PhantomReference, 340 refs, 0 refs, 0.0000456 secs]
[PhantomReference, 1509 refs, 0 refs, 0.0001845 secs]
[PhantomReference, 1484 refs, 0 refs, 0.0004843 secs]
[PhantomReference, 1462 refs, 0 refs, 0.0001473 secs]
[PhantomReference, 790 refs, 0 refs, 0.0000935 secs]
[PhantomReference, 1418 refs, 0 refs, 0.0001702 secs]
[PhantomReference, 1342 refs, 0 refs, 0.0002392 secs]
可以很明显的看到,FinalReference基本没有什么影响,但是PhantomReference回收的数量呈现数十倍的降低!
原因是,FinalReference一旦被创建,将会被jvm强引用,直至被全局最终引用线程所处理。而PhantomReference在被手动释放后,不会被加到全局最终引用队列,因此gc时处理的PhantomReference对象数量被大量减少。
采用PhantomReference,可以有效减少gc时stw的消耗,虽然需要更多的代码,但是在有需求的时候,是值得的。
代码:code
参考:reference