9

具体的な使用例: バイナリ データの抽象化があり、任意のサイズのバイナリ BLOB を処理するために広く使用されています。抽象化は VM の外部のことを考慮せずに作成されたため、既存の実装はライフサイクルをガベージ コレクターに依存しています。

ここで、オフヒープ ストレージ (一時ファイルなど) を使用する新しい実装を追加したいと考えています。抽象化を使用する既存のコードが多数あるため、明示的なライフ サイクル管理のための追加メソッドを導入することは非現実的です。新しいライフ サイクル要件を確実に管理するために、使用するすべてのクライアント ユース ケースを書き直すことはできません。

2 つのソリューション アプローチを考えることができますが、どちらが優れているかを判断することはできません。

a.) 関連するリソースのライフサイクルを管理するための finalize() の使用 (たとえば、一時ファイルはファイナライズで削除されます。これは実装が非常に簡単に思えます。

b.) 参照キューと java.lang.Reference の使用 (ただし、どちらが弱いかファントムか?) 参照がキューに入れられたときにファイルを削除する追加のオブジェクトを使用します。これを実装するにはもう少し手間がかかるようです。新しい実装を作成するだけでなく、そのクリーンアップ データ分離し、ユーザーに公開されたオブジェクトの前にクリーンアップ オブジェクトを GC できないようにする必要があります。 .

c.) 私が考えたことのない他の方法はありますか?

どのアプローチを採用する必要がありますか (そして、なぜそれを好む必要があるのですか)? 実装のヒントも歓迎します。


編集: 必要な信頼度 - VM が突然終了した場合に一時ファイルがクリーンアップされていない場合、私の目的では完全に問題ありません。主な懸念事項は、VM の実行中にローカル ディスクが (数日間にわたって) 一時ファイルでいっぱいになる可能性があることです (これは、テキストを抽出するときに一時ファイルを作成する apache TIKA で実際に発生しました)。特定のドキュメント タイプからは、zip ファイルが原因であると私は信じています)。マシンで定期的なクリーンアップをスケジュールしているので、クリーンアップによってファイルが削除されたとしても、短い間隔で定期的に発生しない限り、世界の終わりを意味するわけではありません。

私が判断できる限り、finalize() は Oracale JRE で動作します。また、javadocs を正しく解釈すると、参照は文書化されているとおりに機能する必要があります (OutOfMemoryError がスローされる前に、ソフト/弱い到達可能な参照オブジェクトのみがクリアされない方法はありません)。これは、VM が特定のオブジェクトを長期間再利用しないことを決定する可能性がある一方で、ヒープがいっぱいになったときに再利用する必要があることを意味します。これは、ヒープ上に限られた数のファイル ベースの BLOB しか存在できないことを意味します。VM は、ある時点でそれらをクリーンアップする必要があります。そうしないと、明らかにメモリ不足になります。または、VM が参照をクリアせずに OOM を実行できるようにする抜け穴はありますか?


Edit2: この時点で私が見る限り、 finalize() と Reference の両方が私の目的に対して十分に信頼できるはずですが、GCとの相互作用は死んだオブジェクトを復活させることができないため、 Reference がより良い解決策である可能性があります。影響は少ないはずですか?


Edit3: 通常、VM は長時間 (サーバー環境) 実行されるため、VM の終了または起動 (シャットダウン フックなど) に依存するソリューション アプローチは役に立ちません。

4

4 に答える 4

3

これがEffectiveJavaの関連項目です:ファイナライザーを避けてください

その項目には、@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

ピンチでは、ファイナライザーでそれを行うだけでは、おそらくファイルのかなりの部分を閉じることになる悪い解決策ではありません。それで十分なら、はるかに簡単なので、私はそのルートをたどります。

一方、確実性を求めている場合は、ファイナライザーを使用するのは非常に悪いことです。タイムリーに実行されることは言うまでもなく、それらが実行されることに依存することはできません。この同じ議論は、さまざまな特殊なタイプの参照のクリーンアップにも、より緩く適用されます。それはむしろあなたのアプリケーションとあなたのハードウェアの詳細に依存します、しかし一般にあなたはあなたのディスクがいっぱいになる前に参照がきれいになるという保証はありません。

これは、メモリに保持しているデータ(スペースの大部分を占めている)が大量であるが非常に短命であり、ファイル参照がはるかに長く続く場合に発生する可能性が高くなります。これにより、多くのマイナーなガベージコレクションが発生し、若い世代のスペースがクリーンアップされ、デッドデータが削除され、最終的に多くのファイル参照が促進されますが、ファイル参照などの古い古いオブジェクトをクリアするメジャーなガベージコレクションは発生しません。そのため、それらは無期限に存続します。GCの背景については、こちらをご覧ください。わずかに遅いGCと引き換えに、若い世代のサイズを増やすことで、実際にヒットするファイナライザーの数を改善できる可能性があります。

もっと確実にしたいのなら、私は少し違った方法で問題を解決します。まず、簡単なケースの解決策として、ファイナライザーにクリーンアップを実装します。次に、フォールバックも作成します。ファイルに使用する準備ができている最大容量を決定します。できれば実際に使用すると予想されるよりも大幅に多く、X分ごとに使用している合計容量を監視し、この範囲を超えた場合は削除します。最も古い(最後の書き込み時間による)ファイルの選択。たとえば、最も古い10%。これにより、かなり厳しい上限が与えられます。ファイナライザーがほとんどの問題をキャッチできると期待されるため、ここではチェック頻度を非常に低く保つことができます。

半関連性があると思うもう1つの注意点は、deleteOnExitです。一時ファイルの作成時にこれを呼び出すと、JVMが正常に終了したときに自動的に削除されることが保証されます。ただし、これには欠点があります。JVMは、閉じるまでこのファイルへの参照を保持する必要があるため、メモリリークがわずかに残ります(ファイルあたり1Kだと思います)。それがあなたにとって価値があるかどうかはわかりませんが、役立つかもしれません!

于 2012-09-13T16:58:17.700 に答える
0

ファイルをすばやくクリーンアップすることについて特に心配していない場合は、それfinalizeが最適です。メモリが不足した場合でも、特定のオブジェクトが GC されるという保証はありません (VM は理論的にはヒープの一部しか収集できません)。ただし、オブジェクトが GC された場合はファイナライズされるため、最大で sizeof(heap) / sizeof(in-memory handle) の未ファイナライズ BLOB が存在することがわかり、ディスク使用量が制限されます。かなり弱い境界ですが、あなたには十分かもしれません。

于 2012-09-13T16:24:48.010 に答える