【问题标题】:Reliable method of cleaning up an external resource associated with an Object清理与对象关联的外部资源的可靠方法
【发布时间】:2012-09-18 00:40:55
【问题描述】:

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

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

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

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

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

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

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


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

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


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


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

【问题讨论】:

  • 如何“可靠”?这就是问题所在。有很多方法,但都有可靠性问题。
  • 终结器的问题是你不知道它们何时(或是否)会运行。来自 Effective Java:“依赖终结器来关闭文件是一个严重错误”.
  • 这对你没有帮助,但我得吐槽。该错误首先不包括用于显式确定性清理(并使客户端代码使用它们)的方法。
  • @delnan 我觉得咆哮有点……奇怪。托管语言的核心目标之一难道不是让程序员从显式管理生命周期中解放吗?
  • @Durandal 定期内存管理确实是由垃圾收集自动完成的,程序员几乎不必关心它。但对于许多其他资源,如连接、文件、流、从其他设备(如 GPU)分配的内存,甚至足够大的堆上 blob(如大位图),确定性清理非常有用。这些资源的共同点是它们非常有限(因此尽早释放它们是有用的,有时是强制性的),相对稀有(因此生命周期的担忧是可控的)并且通常具有简单的生命周期。

标签: java garbage-collection


【解决方案1】:

这是 Effective Java 中的一个相关项目:Avoid finalizers

包含在该项目中的是建议执行 @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

【讨论】:

  • 我也尝试过基于参考的方案。我走了一条不同的路线(为所有可能的实现使用一个守护线程),但除此之外,我们似乎在一般实现中做出了非常相似的选择。我想我会试试这个,看看效果如何。
  • 要记住的另一种方法是将队列处理挂接到一个通常调用的方法(甚至多个方法)中并取消线程。有关示例,请参阅WeakHashMap...
【解决方案2】:

这是我在基于 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
        }
    }

}

【讨论】:

    【解决方案3】:

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

    【讨论】:

      【解决方案4】:

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

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

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

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

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

      【讨论】:

      • deleteOnExit 对我没有帮助 - 在客户端和服务器中使用相同的代码。服务器通常仅每隔 重新启动一次以安装更新。所以假设虚拟机永远不会退出。抱歉,我没有具体说明。
      • 很公平,废弃那一点。尽管如此,如果您确实想要保证,后台清理线程可以为您提供所需的东西,您可能会通过它的声音来做到这一点。如果您对文件的写入频率有一个上限,您实际上可以进一步改进它,因为您可以在检查时删除在那段时间内未写入的所有内容。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-10-24
      • 2011-03-18
      • 2012-02-28
      • 2016-03-04
      • 2019-03-27
      • 1970-01-01
      • 2021-10-15
      相关资源
      最近更新 更多