使用终结器清理弱引用缓存?
cleaning up weak reference caches with finalizers?
假设我有一个由弱引用或软引用组成的缓存。
那些 weak/soft 引用需要在某些时候关闭。
理想情况下,一旦对象被 GC 从缓存中移除,对象就应该被关闭。
使用finalizer/cleaner关闭那些资源,同时在程序结束时仍然循环缓存并手动关闭它们是否合适?
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();
}
}
}
垃圾处理(以及最终确定)是不确定的并且反复无常,因此我建议您不要将软件的任何重要功能都基于它。
无法保证您的对象何时会完成(或清理),甚至 是否 也无法保证它们会完成.只是为了诊断以外的任何目的尽量完全避免它,即生成一条警告级别的日志消息,告诉您一个对象在没有先关闭的情况下正在完成。但是你最好明确地关闭所有内容。
当机器需要更多内存时将缓存的实体从缓存中逐出的想法起初听起来很美,但实际上您会发现:
如果您的垃圾收集器工作积极,(64 位 JVM 上的默认行为)您的实体将比您希望的更频繁地被驱逐,(在您开始 运行内存,) while
如果您的垃圾收集器懒惰地工作,(取决于 JVM 启动选项)您的应用程序可能 运行 在不耗尽其可用内存的情况下完成,然后 JVM 可能会在没有完成或清理任何内容的情况下终止.
一般来说,使用 finalizer
是一个相当糟糕的主意;毕竟它被弃用是有原因的。我认为首先重要的是要了解实现终结器的对象的这种特殊方法 , or why 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
通过以下方式提及:
The cleaning action is invoked only after the associated object becomes phantom reachable, so it is important that the object implementing the cleaning action does not hold references to the object
问题是 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();
}
}
代码看起来有点复杂,实际上并没有那么复杂。我们想做两件主要的事情:
- 将清洁代码分开 class
- 并且 class 不能引用我们正在注册的对象。这是通过没有从
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
假设我有一个由弱引用或软引用组成的缓存。
那些 weak/soft 引用需要在某些时候关闭。
理想情况下,一旦对象被 GC 从缓存中移除,对象就应该被关闭。
使用finalizer/cleaner关闭那些资源,同时在程序结束时仍然循环缓存并手动关闭它们是否合适?
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();
}
}
}
垃圾处理(以及最终确定)是不确定的并且反复无常,因此我建议您不要将软件的任何重要功能都基于它。
无法保证您的对象何时会完成(或清理),甚至 是否 也无法保证它们会完成.只是为了诊断以外的任何目的尽量完全避免它,即生成一条警告级别的日志消息,告诉您一个对象在没有先关闭的情况下正在完成。但是你最好明确地关闭所有内容。
当机器需要更多内存时将缓存的实体从缓存中逐出的想法起初听起来很美,但实际上您会发现:
如果您的垃圾收集器工作积极,(64 位 JVM 上的默认行为)您的实体将比您希望的更频繁地被驱逐,(在您开始 运行内存,) while
如果您的垃圾收集器懒惰地工作,(取决于 JVM 启动选项)您的应用程序可能 运行 在不耗尽其可用内存的情况下完成,然后 JVM 可能会在没有完成或清理任何内容的情况下终止.
一般来说,使用 finalizer
是一个相当糟糕的主意;毕竟它被弃用是有原因的。我认为首先重要的是要了解实现终结器的对象的这种特殊方法
清理某些东西的实际方法是使用 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
通过以下方式提及:
The cleaning action is invoked only after the associated object becomes phantom reachable, so it is important that the object implementing the cleaning action does not hold references to the object
问题是 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();
}
}
代码看起来有点复杂,实际上并没有那么复杂。我们想做两件主要的事情:
- 将清洁代码分开 class
- 并且 class 不能引用我们正在注册的对象。这是通过没有从
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