如何解决使用终结器清理弱引用缓存?
那些弱/软引用需要在某个时候关闭。
理想情况下,对象应在 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();
}
}
代码看起来有点复杂,但实际上并没有那么多。我们要做两件主要的事情:
- 将清理代码放在一个单独的类中
- 并且该类必须没有对我们正在注册的对象的引用。这是通过没有从
InnerState
到CachedObject
的引用并将其设为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 可能会在没有完成或清理任何内容的情况下终止。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。