假设我有一个由弱引用或软引用组成的缓存。
那些弱/软引用需要在某个时刻关闭。
理想情况下,对象应该在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
是一个相当糟糕的主意;毕竟,它被弃用是有原因的。我认为首先重要的是要理解这样一个特殊方法是如何开始工作的,或者为什么实现终结器的对象需要两个周期才能消失。总的来说,这是不确定的,很容易出错,并且使用这种方法可能会遇到意想不到的问题。
清理某些东西的实际方法是使用try with resources
(通过AutoCloseable
),就像:
CachedObject cached = new CachedObject...
try(cached) {
}
但这并不总是一个选择,就像你的情况一样,很可能。我不知道您使用的是什么缓存,但是我们在内部使用我们自己的缓存,它实现了一个所谓的删除侦听器(我们的实现主要基于guava
,并添加了我们自己的少量内容)。所以也许你的缓存也有相同的功能?如果没有,也许你可以换一个有?
如果两者都不是选项,则从java-9开始有Cleaner API。您可以读取它,例如这样做:
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可能会在没有完成或清理任何东西的情况下终止。