1. 概要

このチュートリアルでは、Java言語のコアとなる側面、つまりルートObjectクラスによって提供されるfinalizeメソッドに焦点を当てます。

簡単に言えば、これは特定のオブジェクトのガベージコレクションの前に呼び出されます。

2. ファイナライザーの使用

finalize()メソッドはファイナライザーと呼ばれます。

ファイナライザーは、JVMがこの特定のインスタンスをガベージコレクションする必要があると判断したときに呼び出されます。 このようなファイナライザーは、オブジェクトを生き返らせるなど、あらゆる操作を実行できます。

ただし、ファイナライザーの主な目的は、オブジェクトがメモリから削除される前に、オブジェクトによって使用されているリソースを解放することです。 ファイナライザーは、クリーンアップ操作の主要なメカニズムとして、または他の方法が失敗した場合のセーフティネットとして機能します。

ファイナライザーがどのように機能するかを理解するために、クラス宣言を見てみましょう。

public class Finalizable {
    private BufferedReader reader;

    public Finalizable() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        this.reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    // other class members
}

クラスFinalizableには、クローズ可能なリソースを参照するフィールドreaderがあります。 このクラスからオブジェクトが作成されると、クラスパス内のファイルから読み取る新しいBufferedReaderインスタンスが作成されます。

このようなインスタンスは、 readFirstLine メソッドで使用され、指定されたファイルの最初の行を抽出します。 指定されたコードでリーダーが閉じられていないことに注意してください。

ファイナライザーを使用してこれを行うことができます。

@Override
public void finalize() {
    try {
        reader.close();
        System.out.println("Closed BufferedReader in the finalizer");
    } catch (IOException e) {
        // ...
    }
}

ファイナライザーが通常のインスタンスメソッドと同じように宣言されていることは簡単にわかります。

実際には、 ガベージコレクターがファイナライザーを呼び出す時間は、JVMの実装とシステムの状態に依存しますが、これらは制御できません。

ガベージコレクションをその場で実行するために、System.gcメソッドを利用します。 実際のシステムでは、いくつかの理由から、これを明示的に呼び出すことは絶対にしないでください。

  1. 費用がかかる
  2. ガベージコレクションがすぐにトリガーされるわけではありません。JVMがGCを開始するためのヒントにすぎません。
  3. JVMは、GCを呼び出す必要がある場合によく知っています

GCを強制する必要がある場合は、jconsoleを使用できます。

以下は、ファイナライザーの動作を示すテストケースです。

@Test
public void whenGC_thenFinalizerExecuted() throws IOException {
    String firstLine = new Finalizable().readFirstLine();
    assertEquals("baeldung.com", firstLine);
    System.gc();
}

最初のステートメントでは、 Finalizable オブジェクトが作成され、次にそのreadFirstLineメソッドが呼び出されます。 このオブジェクトはどの変数にも割り当てられていないため、System.gcメソッドが呼び出されたときにガベージコレクションの対象になります。

テストのアサーションは、入力ファイルの内容を検証し、カスタムクラスが期待どおりに機能することを証明するためにのみ使用されます。

提供されたテストを実行すると、ファイナライザーでバッファーされたリーダーが閉じられていることを示すメッセージがコンソールに出力されます。 これは、 finalize メソッドが呼び出され、リソースがクリーンアップされたことを意味します。

この時点まで、ファイナライザーは破棄前の操作に最適な方法のように見えます。 しかし、それは完全に真実ではありません。

次のセクションでは、それらの使用を避ける必要がある理由を説明します。

3. ファイナライザーの回避

それらがもたらす利点にもかかわらず、ファイナライザーには多くの欠点があります。

3.1. ファイナライザーのデメリット

ファイナライザーを使用して重要なアクションを実行するときに直面するいくつかの問題を見てみましょう。

最初の顕著な問題は、迅速性の欠如です。 ガベージコレクションはいつでも発生する可能性があるため、ファイナライザーがいつ実行されるかはわかりません。

ファイナライザーは遅かれ早かれ実行されるので、それ自体は問題ではありません。 ただし、システムリソースは無制限ではありません。 したがって、クリーンアップが行われる前にリソースが不足し、システムがクラッシュする可能性があります。

ファイナライザーは、プログラムの移植性にも影響を与えます。 ガベージコレクションアルゴリズムはJVMの実装に依存するため、プログラムは、あるシステムでは非常にうまく実行され、別のシステムでは異なる動作をする可能性があります。

パフォーマンスコストは、ファイナライザーに伴うもう1つの重要な問題です。 具体的には、 JVMは、空でないファイナライザーを含むオブジェクトを構築および破棄するときに、さらに多くの操作を実行する必要があります。

ここで説明する最後の問題は、ファイナライズ中の例外処理の欠如です。 ファイナライザーが例外をスローすると、ファイナライズプロセスが停止し、通知なしにオブジェクトが破損した状態になります。

3.2. ファイナライザーの効果のデモンストレーション

理論を脇に置いて、ファイナライザーの効果を実際に確認する時が来ました。

空でないファイナライザーを使用して新しいクラスを定義しましょう。

public class CrashedFinalizable {
    public static void main(String[] args) throws ReflectiveOperationException {
        for (int i = 0; ; i++) {
            new CrashedFinalizable();
            // other code
        }
    }

    @Override
    protected void finalize() {
        System.out.print("");
    }
}

finalize()メソッドに注意してください。空の文字列をコンソールに出力するだけです。 このメソッドが完全に空の場合、JVMはオブジェクトをファイナライザーがないかのように扱います。したがって、 finalize()に実装を提供する必要があります。この場合、ほとんど何もありません。

main メソッド内で、forループの各反復で新しいCrashedFinalizableインスタンスが作成されます。 このインスタンスはどの変数にも割り当てられていないため、ガベージコレクションの対象になります。

//他のコードでマークされた行にいくつかのステートメントを追加して、実行時にメモリに存在するオブジェクトの数を確認しましょう。

if ((i % 1_000_000) == 0) {
    Class<?> finalizerClass = Class.forName("java.lang.ref.Finalizer");
    Field queueStaticField = finalizerClass.getDeclaredField("queue");
    queueStaticField.setAccessible(true);
    ReferenceQueue<Object> referenceQueue = (ReferenceQueue) queueStaticField.get(null);

    Field queueLengthField = ReferenceQueue.class.getDeclaredField("queueLength");
    queueLengthField.setAccessible(true);
    long queueLength = (long) queueLengthField.get(referenceQueue);
    System.out.format("There are %d references in the queue%n", queueLength);
}

指定されたステートメントは、内部JVMクラスの一部のフィールドにアクセスし、100万回の反復ごとにオブジェクト参照の数を出力します。

mainメソッドを実行してプログラムを起動しましょう。 無期限に実行されると予想される場合がありますが、そうではありません。数分後、次のようなエラーでシステムがクラッシュするはずです。

...
There are 21914844 references in the queue
There are 22858923 references in the queue
There are 24202629 references in the queue
There are 24621725 references in the queue
There are 25410983 references in the queue
There are 26231621 references in the queue
There are 26975913 references in the queue
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at java.lang.ref.Finalizer.register(Finalizer.java:91)
    at java.lang.Object.<init>(Object.java:37)
    at com.baeldung.finalize.CrashedFinalizable.<init>(CrashedFinalizable.java:6)
    at com.baeldung.finalize.CrashedFinalizable.main(CrashedFinalizable.java:9)

Process finished with exit code 1

ガベージコレクターがうまく機能しなかったようです。システムがクラッシュするまで、オブジェクトの数は増え続けました。

ファイナライザーを削除した場合、参照の数は通常0になり、プログラムは永久に実行され続けます。

3.3. 説明

ガベージコレクターがオブジェクトを適切に破棄しなかった理由を理解するには、JVMが内部でどのように機能するかを調べる必要があります。

ファイナライザーを持つオブジェクト(リファレントとも呼ばれる)を作成する場合、JVMはタイプjava.lang.ref.Finalizerの付随する参照オブジェクトを作成します。 指示対象がガベージコレクションの準備ができたら、JVMは参照オブジェクトを処理の準備ができているとマークし、それを参照キューに入れます。

このキューには、java.lang.ref.Finalizerクラスの静的フィールドqueueを介してアクセスできます。

その間、 Finalizer と呼ばれる特別なデーモンスレッドが実行を継続し、参照キュー内のオブジェクトを探します。 1つが見つかると、キューから参照オブジェクトを削除し、指示対象のファイナライザーを呼び出します。

次のガベージコレクションサイクル中に、参照オブジェクトから参照されなくなったときに、指示対象は破棄されます。

この例で起こったことであるように、スレッドがオブジェクトを高速で生成し続ける場合、Finalizerスレッドは追いつくことができません。 最終的に、メモリはすべてのオブジェクトを格納できなくなり、OutOfMemoryErrorになります。

このセクションに示すように、オブジェクトがワープ速度で作成される状況は、実際にはあまり発生しないことに注意してください。 ただし、これは重要なポイントを示しています。ファイナライザーは非常に高価です。

4. ファイナライザーなしの例

finalize()メソッドを使用せずに、同じ機能を提供するソリューションを調べてみましょう。 以下の例は、ファイナライザーを置き換える唯一の方法ではないことに注意してください。

代わりに、重要なポイントを示すために使用されます。ファイナライザーを回避するのに役立つオプションが常にあります。

新しいクラスの宣言は次のとおりです。

public class CloseableResource implements AutoCloseable {
    private BufferedReader reader;

    public CloseableResource() {
        InputStream input = this.getClass()
          .getClassLoader()
          .getResourceAsStream("file.txt");
        reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    @Override
    public void close() {
        try {
            reader.close();
            System.out.println("Closed BufferedReader in the close method");
        } catch (IOException e) {
            // handle exception
        }
    }
}

新しいCloseableResourceクラスと以前のFinalizableクラスの唯一の違いは、ファイナライザー定義ではなくAutoCloseableインターフェースの実装であることを理解するのは難しいことではありません。

CloseableResourcecloseメソッドの本体は、クラスFinalizableのファイナライザーの本体とほぼ同じであることに注意してください。

以下は、入力ファイルを読み取り、ジョブの終了後にリソースを解放するテストメソッドです。

@Test
public void whenTryWResourcesExits_thenResourceClosed() throws IOException {
    try (CloseableResource resource = new CloseableResource()) {
        String firstLine = resource.readFirstLine();
        assertEquals("baeldung.com", firstLine);
    }
}

上記のテストでは、CloseableResourceインスタンスがtry-with-resourcesステートメントのtryブロックに作成されるため、try-with-resourcesブロックが完了するとそのリソースは自動的に閉じられます。実行。

指定されたテストメソッドを実行すると、CloseableResourceクラスのcloseメソッドから出力されたメッセージが表示されます。

5。 結論

このチュートリアルでは、Javaのコアコンセプトであるfinalizeメソッドに焦点を当てました。 これは紙の上では便利に見えますが、実行時に醜い副作用を引き起こす可能性があります。 そして、さらに重要なことに、ファイナライザーを使用するための代替ソリューションが常にあります。

注意すべき重要な点の1つは、 finalizeがJava9以降非推奨になり、最終的には削除されることです。

いつものように、このチュートリアルのソースコードはGitHubにあります。