虚引用实现资源回收

资源回收

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();
    }    
}

Finalizer

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资源的回收。

Phantom(虚引用)

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 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
                }
            }
        }
    }
}
类分析
  • ReferedResource继承自Resource,代表资源对象的封装。
  • PhantomHandle继承自ResourceFacade,持有Resource对象,实现了接口的dispose()方法,用来做回收操作。
  • PhantomResourceRef继承自PhantomReference,持有对PhantomHandle的虚引用。
我们从PhantomResourceFactory.newResource()方法说起:
  1. 首先创建资源对象resource。
  2. 全局强引用集合GLOBAL_RESOURCES中添加资源对象,防止资源对象被gc。
  3. 创建PhantomHandle对象,持有resource的强引用。
  4. 创建PhantomResourceRef对象ref,持有handle的虚引用。
  5. resource对象持有ref的强引用。
  6. 返回handle对象。
经过newResource()方法之后,生成如下对象引用关系:
  • handle持有resource强引用,对外获得的resource对象均是handle持有的resource对象。
  • GLOBAL_RESOURCES持有resource强引用,保证了无论handle对象是否被gc,resource对象均不会被gc。
  • ref持有handle的虚引用,在handle被gc时,ref对象会被加入REF_QUEUE。
  • ref的构造函数中可以看到,ref还持有了handle持有的resource的强引用,用来保证handle被gc后,ref仍然可以拿到resource对象。
  • resource对象持有ref的强引用。保证ref不会被gc。
GC关系链
  • handle的引用在外部,内部仅有handle对象的虚引用,因此当外部释放handle强引用后,handle对象可以被gc。
  • resource对象被GLOBAL_RESOURCES持有,因此resource对象在从GLOBAL_RESOURCES中释放之前不会被gc回收。
  • resource对象持有PhantomResourceRef的引用,因此PhantomResourceRef对象不会被gc回收。
当外部释放handle所有引用,handle被gc时,会触发如下动作:
  1. handle被gc,ref对象被加入REF_QUEUE。
  2. REF_THREAD线程从REF_QUEUE队列中获取ref实例,调用dispose()方法。
  3. PhantomResourceRef的dispose()方法中,调用了resource的dispose()方法。
  4. resource实际为ReferedResource对象,相当于调用了ReferedResource的dispose()方法。
  5. ReferedResource的dispose()方法中,释放PhantomResourceRef的引用,然后调用父类resource的dispose()方法,实现资源回收,最后,从全局资源强引用集合中删除自己。
  6. 至此,handle对象已经被回收,resource对象没有任何内部强引用,可以被回收,PhantomResourceRef对象不再持有handle虚引用(其实handle已经被回收),也没有人持有PhantomResourceRef引用,也可以被回收,资源完成了完全回收。

finilaizers如何工作

其实,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

你可能感兴趣的:(java)