Java

【Java】例外処理中でopenした場合にメモリリークしないコードを書く方法

こんにちは、ともです。

今回は、例外処理でメモリリークしないように書く方法について記事にして残したいと思います。まず、メモリリークについてご存知でしょうか?

私はメモリリークする危険なコードを書いてしまったため、最近のコードレビュー時に指摘して頂きました。

まずは、メモリリークとはという部分から記事を書き、次に適切な例外処理の書き方について書いていきたいと思います。

メモリリークとは

メモリを解放するようなコードありますよね。Fileの読み込みやDBにアクセスする際の処理で、open()やclose()を行うと思います。

このopen()メソッドが実行されると、メモリにファイルの情報が格納されます。その後、一通りの処理を終えると、close()を呼び出すことでファイル情報を格納していたメモリをクリアする必要があります。

では、ここでopen()後にclose()が呼ばれることなくプログラムが終了した場合はどうなるでしょうか。メモリにゴミが残ったままになりますね。

この現象がメモリリークと呼ばれるものです。Javaではガベージコレクションという機能でこのメモリリークをクリアにすることが可能です。

しかし、メモリリークが発生するようなコードを残しておく事は危険であることが分かると思います。無駄にメモリが消費されパフォーマンスの低下やPCが壊れる恐れがありますね。

メモリリークが発生するコード

メモリリークが発生する可能性があるコードを見ていきましょう。

package MemoryLeak;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class MemoryLeak {
	public static void main(String[] args) {
		try {
			FileReader fr = new FileReader("XXX/test.txt");
			BufferedReader br = new BufferedReader(fr);
			String text = br.readLine();
			System.out.println(text);
			try {
				if(fr != null)fr.close();
				if(br != null)br.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e1) {
			e1.printStackTrace();
		}
	}
}

text.txtというファイルに書かれた1行を読み込んで出力するコードです。

ここで危険な部分が、どこか分かるでしょうか?

メモリリークの原因となる箇所

br.close()とfr.close()が必ず呼ばれるコードになっているでしょうか。図はFileNotFoundExceptionが発生した場合に実行されるコード(緑)と実行されないコード(赤)を示しています。例外発生時にfr.closeが実行されていないことが図から分かると思います。

try句で例外発生時は、発生箇所以降は実行されないからです。

ではどのようにしてコードを書くべきなのでしょうか。

必ずcloseさせる方法

closeさせる方法は簡単です。

  • finally句でclose
  • try with resourcesでclose

finally句でclose

package MemoryLeak;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class MemoryLeak {
	public static void main(String[] args) {
		FileReader fr = null;
		BufferedReader br = null;
		try {
			fr = new FileReader("/Users/matsukawa/eclipse-workspace/GoF/src/MemoryLeak/test.txt");
			br = new BufferedReader(fr);
			String text = br.readLine();
			System.out.println(text);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e1) {
			e1.printStackTrace();
		} finally {
			try {
				if(fr != null)fr.close();
				if(br != null)br.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

finally句でclose()を実行してあげましょう。finally句内のメソッドは必ず実行されます。ですので、finally句でclose()を実行してあげれば、メモリリークは起きませんね。

try-with-resourcesでclose

try-with-resourcesという構文がJava7以降から利用可能です。

package MemoryLeak;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;

public class MemoryLeak {
	public static void main(String[] args) {
		try (FileReader fr = new FileReader("XXXX/test.txt");//←注目
			BufferedReader br = new BufferedReader(fr);){//←注目
			String text = br.readLine();
			System.out.println(text);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e1) {
			e1.printStackTrace();
		}
	}
}

try句にカッコをつけて、その中でインスタンスを生成してあげます。プログラムの実行が終了した場合に閉じなければならないオブジェクトをこの箇所でnewしています。

java.io.Closeable を実装しているすべてのオブジェクトも含め、java.lang.AutoCloseable インタフェースを実装しているオブジェクトはリソースとして使用できます。

引用:Oracleのドキュメント

java.lang.AutoCloseableを実装していないと、この箇所でインスタンス生成できません。あたりえと言えば当たり前ですが、close()を実装しているオブジェクトでないと、この場所でインスタンス生成できません。

まとめ

最近コードレビューをして頂いた際に、教えて頂いたことを備忘録として残しました。

メモリをopen・closeする際には、メモリリークに気をつけながらコードを書きましょう。