Javaのスレッドとメモリリーク

作成:2005年4月18日

補遺:2005年6月11日

吉田誠一のホームページ   >   ソフトウェア工学   >   技術コラム   >   プログラミング

Javaにはgarbage collectorが組み込まれていますので、CやC++の場合と違って、自分でメモリを解放する必要はありません。使われなくなったオブジェクトは、そのうち自動的に削除されます。

ところが、プログラマが意識していないところで、オブジェクトが参照されたままとなってしまい、いつまでたってもgarbage collectorでメモリが解放されない、という状況になることがあります。これが、Javaのメモリリークです。

スレッドにおいても、メモリリークが発生しやすいケースがあります。

例として、次のソースコードを見てください。

public class ThreadTest {
        public static void main ( String[] args ) throws Exception {
                for (int i = 0 ; i < 100 ; i++) {
                        Thread thread = new Thread(new MyOperation());
                        thread.start();

                        while (thread.isAlive()) {
                                thread.sleep(500);
                        }
                }
        }
}

public class MyOperation implements Runnable {
        protected double[] huge_buffer = null;

        public void run ( ) {
                huge_buffer = new double[1000000];      // 8MB
        }
}
      

これは、8MBのバッファを100回確保するだけのプログラムです。

Threadオブジェクトへの参照を持つthread変数のスコープは、forループの内側だけです。1つのスレッドの処理が終わり、ループが回るたびに、スレッドオブジェクトは参照されなくなり、garbage collectorによってメモリが解放されます。8MBのバッファを持つMyOperationオブジェクトは、スレッドオブジェクトだけから参照されているため、スレッドオブジェクトとともにメモリから解放されます。

そのため、このプログラムを実行しても、OutOfMemoryErrorにはなりません。

ここで、MyOperationクラスを、Runnableインターフェースを実装するのではなく、Threadクラスを継承するように書き換えてみます。

public class ThreadTest {
        public static void main ( String[] args ) throws Exception {
                for (int i = 0 ; i < 100 ; i++) {
                        Thread thread = new Thread(new MyOperation());
                        thread.start();

                        while (thread.isAlive()) {
                                thread.sleep(500);
                        }
                }
        }
}

public class MyOperation extends Thread {
        protected double[] huge_buffer = null;

        public void run ( ) {
                huge_buffer = new double[1000000];      // 8MB
        }
}
      

Threadクラス自身がRunnableインターフェースを実装(implements Runnable)しているため、MyOperationクラスの定義だけを修正すれば、mainメソッドは修正しなくても、動きます。ところが、このプログラムを実行すると、OutOfMemoryErrorが発生してしまいます。

これは、Threadクラスが、次のような仕様になっているためです。

Threadクラスのソースコードを見てみると、コンストラクタが呼ばれた時点で、ThreadGroupオブジェクトのaddメソッドを呼び出し、自分自身を登録しています。また、exitメソッドの中で、ThreadGroupオブジェクトのremoveメソッドを呼び出し、自分自身への参照を削除しています。

MyOperationクラスをThreadクラスの子クラスとした例では、forループの中で、2つのThreadオブジェクトが生成され、それぞれ、ThreadGroupオブジェクトから参照されるようになっています。しかし、startメソッドは、threadという変数で参照しているThreadオブジェクトに対してしか、呼び出されていません。そのため、8MBのバッファを持つMyOperationオブジェクトは、forループを抜けてもThreadGroupオブジェクトから参照されたままとなり、メモリから解放されないのです。

オブジェクトの参照関係

JDKが提供するThreadクラスは、Runnableインターフェースを渡すことで、任意の処理をスレッドとして実行できるように作られています。そして、Threadクラス自身もRunnableインターフェースを実装しているため、このように、ThreadオブジェクトにThreadオブジェクトを渡す、ということもできてしまいます。しかし、実際には、メモリリークが発生してしまう結果となります。

サンマイクロシステムズが行っているJava認定資格(SJC-P)でも、Threadオブジェクトにスレッドオブジェクトを渡した時の挙動の問題が出題されていました。

この問題では、『問題なく、実行される』というのが正解とされていました。しかし、実際には、メモリリークが発生しているはずなので、語弊があるかもしれません。

ThreadクラスがRunnableインターフェースを実装している理由は何でしょうか? Threadクラス自身も、Runnableインターフェースで定義されているrun()メソッドを持っているためでしょうか?

しかし、Threadオブジェクトのコンストラクタに、ThreadオブジェクトをRunnableのつもりで渡すと、このように、メモリリークが発生してしまいます。Threadクラスは、Threadオブジェクトを受け取って動作するようには、作られていないのです。

それを考えると、Threadクラスは、Runnableインターフェースを実装しない方が良かったのではないか、と思えます。

補遺:2005年6月11日

尾田晃様から、次のようなご指摘を頂きました。

実はこれは J2SE 1.4 以前の話で、J2SE 5.0 では java.lang.Thread の start メソッド内で、java.lang.ThreadGroup の add メソッドを呼んでいるようです。

なので、上記で示されているソースコードは J2SE 5.0 では OutOfMemoryErrorを起こすことなく(メモリリークを起こすことなく)終了します。

ここで指摘した問題は、J2SE 5.0 で解消したようです。

Copyright(C) Seiichi Yoshida ( comet@aerith.net ). All rights reserved.