9

具体用例:二进制数据有一个抽象,广泛用于处理任意大小的二进制 blob。由于抽象是在不考虑VM之外的事物的情况下创建的,因此现有实现依赖于垃圾收集器的生命周期。

现在我想添加一个使用堆外存储(例如在临时文件中)的新实现。由于有很多使用抽象的现有代码,因此为显式生命周期管理引入额外的方法是不切实际的,我无法重写每个客户端用例以确保它们管理新的生命周期需求。

我可以想到两种解决方案,但无法确定哪一种更好:

a.) 使用 finalize() 来管理相关资源的生命周期(例如,在 finalize 中删除临时文件。这似乎很容易实现。

b.) 使用引用队列和 java.lang.Reference (但哪个,弱或幻像?)与一些额外的对象,当引用入队时删除文件。这似乎需要更多的工作来实现,我不仅需要创建新的实现,还需要分离出它的清理数据,并确保清理对象在暴露给用户的对象之前不能被 GC .

c.) 我还没有想到的其他方法?

我应该采用哪种方法(为什么我更喜欢它)?也欢迎实施提示。


编辑:所需的可靠性程度 - 就我的目的而言,如果在 VM 突然终止的情况下清理临时文件,则它非常好。主要担心的是,当 VM 运行时,它很可能会用临时文件(在几天的过程中)填满本地磁盘(这在我身上发生过 apache TIKA,它在提取文本时创建了临时文件在某些文档类型中,我认为 zip 文件是罪魁祸首)。我在机器上安排了定期清理,因此如果文件因清理而丢失,这并不意味着世界末日 - 只要它不会在短时间内定期发生。

据我所知,finalize() 适用于 Oracale JRE。而且,如果我正确解释了 javadocs,则引用必须按记录工作(在抛出 OutOfMemoryError 之前,不可能不清除仅软/弱可访问的引用对象)。这意味着虽然 VM 可能决定在很长一段时间内不回收特定对象,但它必须在堆满时才这样做。反过来,这意味着堆上只能存在有限数量的基于文件的 blob。虚拟机必须在某个时候清理它们,否则它肯定会耗尽内存。或者是否有任何漏洞允许VM在不清除引用的情况下运行OOM(假设它们不再被引用)?


Edit2:据我目前所见,finalize() 和 Reference 对于我的目的来说应该足够可靠,但我认为 Reference 可能是更好的解决方案,因为它与 GC 的交互不能恢复死对象,因此它的性能影响应该更小吧?


Edit3:依赖于 VM 终止或启动(关闭挂钩或类似)的解决方案方法对我没有用,因为通常 VM 运行时间较长(服务器环境)。

4

4 回答 4

3

这是来自Effective Java的相关项目:避免终结器

该项目中包含的建议是执行@delnan 在评论中建议的建议:提供显式终止方法。还提供了大量示例:InputStream.close()Graphics.dispose()等。了解奶牛可能已经离开了那个谷仓......

无论如何,这里有一个关于如何使用参考对象来完成的草图。一、二进制数据的接口:

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();
    }
}

然后,创建和跟踪基于文件的 blob 的工厂:

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");
        }
    }
}

最后,一个包含一些人工 GC“压力”的测试来让事情顺利进行:

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
于 2012-09-14T20:27:16.840 回答
1

这是我在基于 kschneids 参考的示例之后制定的解决方案(以防万一有人需要通用可用的实现)。它记录在案并且应该易于理解/适应:

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
        }
    }

}
于 2012-09-17T16:40:55.837 回答
0

如果您不是特别担心快速清理文件,那么finalize就是要走的路。不能保证任何特定对象都会被 GC,即使内存不足(理论上 VM 只能收集堆的一部分)。但是,如果一个对象经过 GC,它将被最终确定,因此您知道您最多会有 sizeof(heap) / sizeof(in-memory handle) 未最终确定的 blob,这对您的磁盘使用造成了一定的限制。这是一个非常弱的界限,但听起来它对你来说可能已经足够好了。

于 2012-09-13T16:24:48.010 回答
0

在紧要关头,仅在终结器中执行此操作并不是一个糟糕的解决方案,它可能至少会关闭大部分文件。如果这足够好,我会沿着这条路走,因为它会容易得多。

另一方面,如果您正在寻找任何确定性,那么使用终结器是非常糟糕的。你不能依赖它们一直运行,更不用说及时运行了,同样的论点也更松散地适用于清理各种特殊类型的引用。这取决于您的应用程序和硬件的详细信息,但通常您不能保证在磁盘填满之前清除引用。

如果您保存在内存中的数据(占用大部分空间)很大但寿命很短,而文件引用的持续时间更长,则更有可能发生这种情况。这会导致大量次要垃圾回收,这将清理年轻代空间,删除死数据并最终提升许多文件引用,但不会导致重大垃圾回收,这将清除旧的终身对象,例如文件引用,所以那些然后无限期地活着。看看这个以获得更多的 GC 背景。你可以通过增加年轻代的大小来提高你的终结器实际受到攻击的数量,以换取稍微慢一点的 GC。

如果您确实想要更多确定性,我会稍微不同地解决问题。首先,在终结器中实施清理作为一种快速的简单案例解决方案。然后也建立一个后备;决定您准备让文件占用的最大空间量,最好比您预期实际使用的空间大得多,每 X 分钟监控一次您使用的总空间,如果超过此界限,则删除选择最旧的(按最后写入时间)文件,例如最旧的 10%。这为您提供了一个相当硬的上限,并且您可以在这里将检查频率保持在非常低的水平,因为终结器应该有望捕获大多数问题。

我认为可能是半相关的另一个注意事项是deleteOnExit。在创建临时文件时调用它们将保证在 JVM 成功退出时自动删除它们。但是,这确实有缺点:JVM 必须保持对这个文件的引用,直到它关闭,这会给你留下一个小的内存泄漏(我相信每个文件 1K)。不确定这是否对您有价值,但可能会有所帮助!

于 2012-09-13T16:58:17.700 回答