1. 序章

Javaでファイルを操作する際の一般的な落とし穴は、使用可能なファイル記述子が不足する可能性です。

このチュートリアルでは、この状況を見て、この問題を回避する2つの方法を提供します。

2. JVMがファイルを処理する方法

JVMは、オペレーティングシステムから私たちを隔離する優れた仕事をしますが、ファイル管理などの低レベルの操作をOSに委任します。

これは、Javaアプリケーションで開くファイルごとに、オペレーティングシステムがファイル記述子を割り当ててファイルをJavaプロセスに関連付けることを意味します。 JVMがファイルの処理を終了すると、記述子を解放します。

それでは、例外をトリガーする方法について詳しく見ていきましょう。

3. ファイル記述子のリーク

Javaアプリケーションのすべてのファイル参照について、OSに対応するファイル記述子があることを思い出してください。 この記述子は、ファイル参照インスタンスが破棄された場合にのみ閉じられます。 これはガベージコレクションフェーズ中に発生します。

ただし、参照がアクティブなままで、開いているファイルが増えると、最終的にOSは割り当てるファイル記述子を使い果たします。 その時点で、この状況がJVMに転送され、IOExceptionがスローされます。

この状況は、短い単体テストで再現できます。

@Test
public void whenNotClosingResoures_thenIOExceptionShouldBeThrown() {
    try {
        for (int x = 0; x < 1000000; x++) {
            FileInputStream leakyHandle = new FileInputStream(tempFile);
        }
        fail("Method Should Have Failed");
    } catch (IOException e) {
        assertTrue(e.getMessage().containsIgnoreCase("too many open files"));
    } catch (Exception e) {
        fail("Unexpected exception");
    }
}

ほとんどのオペレーティングシステムでは、JVMプロセスはループを完了する前にファイル記述子を使い果たし、それによってIOExceptionをトリガーします。

適切なリソース処理でこの状態を回避する方法を見てみましょう。

4. 取り扱いリソース

前に述べたように、ファイル記述子はガベージコレクション中にJVMプロセスによって解放されます。

ただし、ファイル参照を適切に閉じなかった場合、コレクターはその時点で参照を破棄しないことを選択し、記述子を開いたままにして、開くことができるファイルの数を制限する可能性があります。

ただし、ファイルを開いた場合に、不要になったときに確実に閉じるようにすることで、この問題を簡単に取り除くことができます。

4.1. 参照を手動で解放する

参照を手動で解放することは、JDK8より前の適切なリソース管理を確実にするための一般的な方法でした。

開いているファイルを明示的に閉じる必要があるだけでなく、コードが失敗して例外がスローされた場合でも確実に閉じるようにします。 これは、finallyキーワードを使用することを意味します。

@Test
public void whenClosingResoures_thenIOExceptionShouldNotBeThrown() {
    try {
        for (int x = 0; x < 1000000; x++) {
            FileInputStream nonLeakyHandle = null;
            try {
                nonLeakyHandle = new FileInputStream(tempFile);
            } finally {
                if (nonLeakyHandle != null) {
                    nonLeakyHandle.close();
                }
            }
        }
    } catch (IOException e) {
        assertFalse(e.getMessage().toLowerCase().contains("too many open files"));
        fail("Method Should Not Have Failed");
    } catch (Exception e) {
        fail("Unexpected exception");
    }
}

finally ブロックは常に実行されるため、参照を適切に閉じる機会が与えられ、開いている記述子の数が制限されます。

4.2. try-with-resourcesを使用する

JDK 7は、リソースの廃棄を実行するためのよりクリーンな方法を提供します。 これは一般にtry-with-resourcesとして知られており、 try 定義にリソースを含めることで、リソースの破棄を委任できます。

@Test
public void whenUsingTryWithResoures_thenIOExceptionShouldNotBeThrown() {
    try {
        for (int x = 0; x < 1000000; x++) {
            try (FileInputStream nonLeakyHandle = new FileInputStream(tempFile)) {
                // do something with the file
            }
        }
    } catch (IOException e) {
        assertFalse(e.getMessage().toLowerCase().contains("too many open files"));
        fail("Method Should Not Have Failed");
    } catch (Exception e) {
        fail("Unexpected exception");
    }
}

ここでは、tryステートメント内でnonLeakyHandleを宣言しました。 そのため、Javaは、を最終的に使用する必要がなく、リソースを閉じます。

5. 結論

ご覧のとおり、開いているファイルを適切に閉じないと、プログラム全体に影響を与える複雑な例外が発生する可能性があります。 適切なリソース処理により、この問題が発生しないようにすることができます。

この記事の完全なソースコードは、GitHubから入手できます。