Java 清理与对象关联的外部资源的可靠方法
具体用例:二进制数据有一个抽象,广泛用于处理任意大小的二进制blob。由于抽象是在不考虑VM之外的事物的情况下创建的,因此现有的实现在其生命周期中依赖于垃圾收集器 现在我想添加一个使用堆外存储(例如,在临时文件中)的新实现。因为有很多现有的代码都使用了抽象,所以引入额外的方法来进行显式的生命周期管理是不切实际的,我不能重写每个客户机用例来确保他们管理新的生命周期需求 我可以想出两种解决方案,但无法决定哪一种更好: a、 )使用finalize()来管理相关资源的生命周期(例如,finalize中删除了临时文件。这似乎很容易实现 b、 )使用引用队列和java.lang.reference(但哪一个,弱还是幻影?)以及一些额外的对象,这些对象在引用排队时删除文件。这似乎需要更多的工作来实现,我不仅需要创建新的实现,还需要分离出它的清理数据,并确保清理对象不能在对象暴露给用户之前进行GC c、 )还有其他我没想到的方法吗 我应该采取哪种方法(为什么我更喜欢它)?也欢迎提供实现提示Java 清理与对象关联的外部资源的可靠方法,java,garbage-collection,Java,Garbage Collection,具体用例:二进制数据有一个抽象,广泛用于处理任意大小的二进制blob。由于抽象是在不考虑VM之外的事物的情况下创建的,因此现有的实现在其生命周期中依赖于垃圾收集器 现在我想添加一个使用堆外存储(例如,在临时文件中)的新实现。因为有很多现有的代码都使用了抽象,所以引入额外的方法来进行显式的生命周期管理是不切实际的,我不能重写每个客户机用例来确保他们管理新的生命周期需求 我可以想出两种解决方案,但无法决定哪一种更好: a、 )使用finalize()来管理相关资源的生命周期(例如,finalize中
编辑:所需的可靠性程度-出于我的目的,如果在VM突然终止的情况下没有清理临时文件,这是非常好的。主要担心的是,当VM运行时,它很可能会用临时文件填满本地磁盘(在几天的时间内)(这在我的apache TIKA中发生过,它在从某些文档类型提取文本时创建临时文件,我相信zip文件是罪魁祸首)。我在机器上有一个定期的清理计划,所以如果一个文件被清理掉,这并不意味着世界末日——只要它不是在短时间内定期发生 据我所知,finalize()适用于Oracale JRE。如果我正确地解释了javadocs,那么引用必须像文档中描述的那样工作(在抛出OutOfMemoryError之前,不可能不清除一个只有软/弱可访问的引用对象)。这意味着,尽管VM可能决定在很长一段时间内不回收某个特定对象,但它必须在堆满时最迟这样做。反过来,这意味着堆上只能存在数量有限的基于文件的blob。虚拟机必须在某个时候清理它们,否则它肯定会耗尽内存。或者是否存在允许VM在不清除引用的情况下运行OOM的漏洞(假设它们不再是stronly引用)
Edit2:就我目前所见,finalize()和Reference对于我来说都应该足够可靠,但我认为Reference可能是更好的解决方案,因为它与GC的交互不能恢复死对象,因此它对性能的影响应该更小
Edit3:依赖于VM终止或启动(关机挂钩或类似)的解决方案方法对我不适用,因为VM通常会运行较长的时间(服务器环境)。如果您不特别担心快速清理文件,那么
finalize
是一种方法。即使内存不足,也不能保证任何特定对象都会被GC’d(VM理论上只能收集堆的一部分)。但是如果一个对象是GC'd,它将被最终确定,所以您知道您最多会有sizeof(heap)/sizeof(内存句柄)未最终确定的blob,这会对您的磁盘使用造成一定的限制。这是一个相当弱的界限,但听起来对您来说可能已经足够好了。在紧要关头,在终结器中执行这项操作并不是一个坏的解决方案,至少可以关闭大部分文件。如果这足够好的话,我会沿着这条路走,因为这会容易得多
另一方面,如果您正在寻找任何确定性,那么使用终结器是非常糟糕的;您不能指望它们永远都在运行,更不用说及时地运行了,同样的论点在清理各种特殊类型的引用时也适用得更宽松一些。这取决于应用程序和硬件的细节,但一般来说,您无法保证在磁盘填满之前清除引用
如果您在内存中保存的数据(占用了大部分空间)是大量的但寿命很短,而文件引用持续的时间更长,则更可能发生这种情况。这将导致大量次要垃圾收集,这将清理年轻一代的空间,删除死数据并最终提升许多文件引用,但不会导致主要垃圾收集,这将清除旧的终身对象(如文件引用),因此这些对象将无限期保持活动状态。查看更多GC背景。你可以通过增加年轻一代的规模来提高你的终结器中有多少人被击中,以换取稍微慢一点的GCs
如果你真的想要更多的确定性,我会稍微不同地解决这个问题。首先,在终结器中实现清理,作为快速简单的案例解决方案。然后建立一个后备方案;确定您准备让文件占用的最大空间量,最好比您预期实际使用的空间量大得多,每X分钟监控一次您使用的总空间,如果超过此界限,则删除最旧(截至上次写入时间)文件的选择,例如,最旧的10%。这给了您一个相当困难的上限,您可能可以在这里将检查频率保持在非常低的水平,因为终结器有望捕获大多数问题
另一个我认为可能是半相关的注意事项是。卡兰
import java.io.IOException;
public interface Blob {
public byte[] read() throws IOException;
public void update(byte[] data) throws IOException;
}
import java.io.File;
import java.io.IOException;
public class FileBlob implements Blob {
private final File file;
public FileBlob(File file) {
super();
this.file = file;
}
@Override
public byte[] read() throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void update(byte[] data) throws IOException {
throw new UnsupportedOperationException();
}
}
import java.io.File;
import java.io.IOException;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
public class FileBlobFactory {
private static final long TIMER_PERIOD_MS = 10000;
private final ReferenceQueue<File> queue;
private final ConcurrentMap<PhantomReference<File>, String> refs;
private final Timer reaperTimer;
public FileBlobFactory() {
super();
this.queue = new ReferenceQueue<File>();
this.refs = new ConcurrentHashMap<PhantomReference<File>, String>();
this.reaperTimer = new Timer("FileBlob reaper timer", true);
this.reaperTimer.scheduleAtFixedRate(new FileBlobReaper(), TIMER_PERIOD_MS, TIMER_PERIOD_MS);
}
public Blob create() throws IOException {
File blobFile = File.createTempFile("blob", null);
//blobFile.deleteOnExit();
String blobFilePath = blobFile.getCanonicalPath();
FileBlob blob = new FileBlob(blobFile);
this.refs.put(new PhantomReference<File>(blobFile, this.queue), blobFilePath);
return blob;
}
public void shutdown() {
this.reaperTimer.cancel();
}
private class FileBlobReaper extends TimerTask {
@Override
public void run() {
System.out.println("FileBlob reaper task begin");
Reference<? extends File> ref = FileBlobFactory.this.queue.poll();
while (ref != null) {
String blobFilePath = FileBlobFactory.this.refs.remove(ref);
File blobFile = new File(blobFilePath);
boolean isDeleted = blobFile.delete();
System.out.println("FileBlob reaper deleted " + blobFile + ": " + isDeleted);
ref = FileBlobFactory.this.queue.poll();
}
System.out.println("FileBlob reaper task end");
}
}
}
import java.io.IOException;
public class FileBlobTest {
public static void main(String[] args) {
FileBlobFactory factory = new FileBlobFactory();
for (int i = 0; i < 10; i++) {
try {
factory.create();
} catch (IOException exc) {
exc.printStackTrace();
}
}
while(true) {
try {
Thread.sleep(5000);
System.gc(); System.gc(); System.gc();
} catch (InterruptedException exc) {
exc.printStackTrace();
System.exit(1);
}
}
}
}
FileBlob reaper task begin
FileBlob reaper deleted C:\WINDOWS\Temp\blob1055430495823649476.tmp: true
FileBlob reaper deleted C:\WINDOWS\Temp\blob873625122345395275.tmp: true
FileBlob reaper deleted C:\WINDOWS\Temp\blob4123088770942737465.tmp: true
FileBlob reaper deleted C:\WINDOWS\Temp\blob1631534546278785404.tmp: true
FileBlob reaper deleted C:\WINDOWS\Temp\blob6150533076250997032.tmp: true
FileBlob reaper deleted C:\WINDOWS\Temp\blob7075872276085608840.tmp: true
FileBlob reaper deleted C:\WINDOWS\Temp\blob5998579368597938203.tmp: true
FileBlob reaper deleted C:\WINDOWS\Temp\blob3779536278201681316.tmp: true
FileBlob reaper deleted C:\WINDOWS\Temp\blob8720399798060613253.tmp: true
FileBlob reaper deleted C:\WINDOWS\Temp\blob3046359448721598425.tmp: true
FileBlob reaper task end
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
/**
* Helper class for cleaning up resources when an object is
* garbage collected. Use as follows (both anonymous subclass or
* public subclass are fine. Be extra careful to not retain
* a reference to the trigger!):
*
* new ResourceFinalizer(trigger) {
*
* // put user defined state relevant for cleanup here
*
* protected void cleanup() {
* // implement cleanup procedure.
* }
* }
*
* Typical application is closing of native resources when an object
* is garbage collected (e.g. VM external resources).
*
* You must not retain any references from the ResourceFinalizer to the
* trigger (otherwise the trigger can never become eligible for GC).
* You can however retain references to the ResourceFinalizer from the
* trigger, so you can access the data relevant for the finalizer
* from the trigger (no need to duplicate the data).
* There is no need to explicitly reference the finalizer after it has
* been created, the finalizer base class will ensure the finalizer
* itself is not eligible for GC until it has been run.
*
* When the VM terminates, ResourceFinalizer that haven't been
* triggered will run, regardless of the state of their triggers
* (that is even if the triggers are still reachable, the finalizer
* will be called). There are no guarantees on this, if the VM
* is terminated abruptly this step may not take place.
*/
public abstract class ResourceFinalizer {
/**
* Constructs a ResourceFinalizer that is triggered when the
* object referenced by finalizationTrigger is garbage collected.
*
* To make this work, you must ensure there are no references to
* the finalizationTrigger object from the ResourceFinalizer.
*/
protected ResourceFinalizer(final Object trigger) {
// create reference to trigger and register this finalizer
final Reference<Object> reference = new PhantomReference<Object>(trigger, referenceQueue);
synchronized (finalizerMap) {
finalizerMap.put(reference, this);
}
}
/**
* The cleanup() method is called when the finalizationTrigger
* has been garbage collected.
*/
protected abstract void cleanup();
// --------------------------------------------------------------
// ---
// --- Background finalization management
// ---
// --------------------------------------------------------------
/**
* The reference queue used to interact with the garbage collector.
*/
private final static ReferenceQueue<Object> referenceQueue = new ReferenceQueue<Object>();
/**
* Global static map of finalizers. Enqueued references are used as key
* to find the finalizer for the referent.
*/
private final static HashMap<Reference<?>, ResourceFinalizer> finalizerMap =
new HashMap<Reference<?>, ResourceFinalizer>(16, 2F);
static {
// create and start finalizer thread
final Thread mainLoop = new Thread(new Runnable() {
@Override
public void run() {
finalizerMainLoop();
}
}, "ResourceFinalizer");
mainLoop.setDaemon(true);
mainLoop.setPriority(Thread.NORM_PRIORITY + 1);
mainLoop.start();
// add a shutdown hook to take care of resources when the VM terminates
final Thread shutdownHook = new Thread(new Runnable() {
@Override
public void run() {
shutdownHook();
}
});
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
/**
* Main loop that runs permanently and executes the finalizers for
* each object that has been garbage collected.
*/
private static void finalizerMainLoop() {
while (true) {
final Reference<?> reference;
try {
reference = referenceQueue.remove();
} catch (final InterruptedException e) {
// this will terminate the thread, should never happen
throw new RuntimeException(e);
}
final ResourceFinalizer finalizer;
// find the finalizer for the reference
synchronized (finalizerMap) {
finalizer = finalizerMap.remove(reference);
}
// run the finalizer
callFinalizer(finalizer);
}
}
/**
* Called when the VM shuts down normally. Takes care of calling
* all finalizers that haven't been triggered yet.
*/
private static void shutdownHook() {
// get all remaining resource finalizers
final List<ResourceFinalizer> remaining;
synchronized (finalizerMap) {
remaining = new ArrayList<ResourceFinalizer>(finalizerMap.values());
finalizerMap.clear();
}
// call all remaining finalizers
for (final ResourceFinalizer finalizer : remaining) {
callFinalizer(finalizer);
}
}
private static void callFinalizer(final ResourceFinalizer finalizer) {
try {
finalizer.cleanup();
} catch (final Exception e) {
// don't care if a finalizer throws
}
}
}