使用终结器清理弱引用缓存?

问题描述

假设我有一个包含弱引用或软引用的缓存。

那些弱/软引用需要在某个时候关闭

理想情况下,对象应在 GC 从缓存中移除后立即关闭

使用终结器/清理器关闭这些资源,同时在程序结束时仍然循环缓存并手动关闭它们是否合适?

public void Cachedobject implements AutoClosable{
    private boolean open;//getter
    public Cachedobject{
        //Create resource
        open=true;
    }
    @Override
    public void finalize(){
        super.finalize();
        if(open){
            try{
                close();
             }catch(IllegalStateException e){
                 //Log
            }
        }
    }
    @Override
    public void close(){
        if(open){
            //Close
            open=false;
        }else{
            throw new IllegalStateException("already closed");
        }
    }
}
private WeakHashMap<Cachedobject,Object> cache=new WeakHashMap<>();

public void close(){
    //Executed when cache is not needed anymore,e.g. program termination
    for(Cachedobject cachedElement:cache){
        if(cachedElement.isopen()){
             cachedElement.close();
        }
    }
}

解决方法

一般来说,使用 finalizer 是一个相当糟糕的主意;毕竟,它被弃用是有原因的。我认为首先重要的是了解这种特殊方法 works to begin with 的机制,或者为什么实现终结器的对象的 it takes two cycles 消失了。总体思路是,这是非确定性的,容易出错,而且您可能会在使用这种方法时遇到意想不到的问题。

清理东西的实际方法是使用try with resources(通过AutoCloseable),就像:

CachedObject cached = new CachedObject...
try(cached) {

} 

但这并不总是一种选择,就像您的情况一样,很可能。我不知道您使用的是什么缓存,但我们在内部使用我们自己的缓存,它实现了所谓的移除侦听器(我们的实现主要基于 guava,并添加了我们自己的少量内容)。那么可能你的缓存有相同的吗?如果没有,您可以切换到一个吗?

如果两者都不是选项,则有 Cleaner API since java-9。您可以阅读它,例如执行以下操作:

static class CachedObject implements AutoCloseable {

    private final String instance;

    private static final Map<String,String> MAP = new HashMap<>();

    public CachedObject(String instance) {
        this.instance = instance;
    }

    @Override
    public void close()  {
        System.out.println("close called");
        MAP.remove(instance);
    }
}

然后尝试使用它,通过:

private static final Cleaner CLEANER = Cleaner.create();

public static void main(String[] args) {

    CachedObject first = new CachedObject("first");
    CLEANER.register(first,first::close);
    first = null;
    gc();
    System.out.println("Done");

}

static void gc(){
    for(int i=0;i<3;++i){
        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
        System.gc();
    }
}

很简单,对吧?也错了。 apiNote 通过以下方式提到这一点:

只有在关联对象变为幻影可达后才会调用清理操作,因此实现清理操作的对象不持有对对象的引用很重要

问题在于 Runnable(在 Cleaner::register 的第二个参数中)捕获 first,并且现在拥有对它的强引用。这意味着永远不会调用清洁。相反,我们可以直接遵循文档中的建议:

static class CachedObject implements AutoCloseable {

    private static final Cleaner CLEANER = Cleaner.create();
    private static final Map<String,String> MAP = new HashMap<>();
    private final InnerState innerState;
    private final Cleaner.Cleanable cleanable;

    public CachedObject(String instance) {
        innerState = new InnerState(instance);
        this.cleanable = CLEANER.register(this,innerState);
        MAP.put(instance,instance);
    }

    static class InnerState implements Runnable {

        private final String instance;

        public InnerState(String instance) {
            this.instance = instance;
        }

        @Override
        public void run() {
            System.out.println("run called");
            MAP.remove(instance);
        }
    }

    @Override
    public void close()  {
        System.out.println("close called");
        cleanable.clean();
    }
}

代码看起来有点复杂,但实际上并没有那么多。我们要做两件主要的事情:

  • 将清理代码放在一个单独的类中
  • 并且该类必须没有对我们正在注册的对象的引用。这是通过没有从 InnerStateCachedObject 的引用并将其设为 static 来实现的。

所以,我们可以测试一下:

 public static void main(String[] args) {

    CachedObject first = new CachedObject("first");
    first = null;
    gc();

    System.out.println("Done");
    System.out.println("Size = " + CachedObject.MAP.size());


 }

 static void gc() {
    for(int i=0;i<3;++i){
        LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
        System.gc();
    }
 }

输出:

run called
Done
Size = 0
,

垃圾处理(因此最终确定)是不确定的且相当反复无常,因此我建议您不要将您的软件的任何重要功能建立在它之上。

无法保证您的对象何时完成(或清理),甚至是否也不能保证它们会被完成。只是为了除诊断之外的任何目的尝试完全避免它,即生成警告级别的日志消息,告诉您一个对象在没有先关闭的情况下正在完成。但你最好明确关闭所有内容。

当机器需要更多内存时将缓存实体从缓存中逐出的想法起初听起来很美好,但实际上您会发现:

  • 如果您的垃圾收集器工作积极,(64 位 JVM 上的默认行为)您的实体将比您希望的更频繁地被驱逐(在您开始耗尽内存之前),同时

  • 如果您的垃圾收集器工作缓慢(取决于 JVM 启动选项),您的应用程序可能会运行到完成而不会耗尽其可用内存,然后 JVM 可能会在没有完成或清理任何内容的情况下终止。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...