Locked History Actions

java/Finalizer

FinalizerとフルGC

問題と解決

次のコードは(無茶ではあるが)、きちんと動作する。

/* 1 */
public class GCTest {
  byte[]a = new byte[3000000];
  public static void main(String[]args) {
    while (true) {
      new Thread() {
        public void run() {          
          GCTest test = new GCTest();
          System.out.println("CREATED!!!");
        }
      }.start();
    }
  }
}

ここに、以下のようにfinalizeを追加すると、各スレッド内でOutofMemoryErrorが発生してGCTestオブジェクトは作成されなくなる。

/* 2 */
public class GCTest {
  byte[]a = new byte[3000000];  
  @Override protected void finalize() throws Throwable { //!!!
    super.finalize(); //!!!
  }  //!!!
  public static void main(String[]args) {
    while (true) {
      new Thread() {
        public void run() {          
          GCTest test = new GCTest();
          System.out.println("CREATED");
        }
      }.start();
    }
  }
}

このとき、ヒープダンプをEclipse Memory Analyzerで表示してみると、以下のような状態が観察される。

Finalizer.png

つまり、ヒープ領域のほとんどをjava.lang.ref.Finalizer(とそこから参照されるオブジェクト)に埋め尽くされてしまっている。

また、Dominator Treeを見てみると状況によっては、Finalizerが入れ子になっている場合がある。

Finalizer2.png

次のようにフルGCを強制することで解決できる(ただし、必ずしもフルGCが起こるとは限らない。System.gc()はあくまでもヒント)。

※<s>System.gc()の代わりにSystem.runFinalization()でもうまくいくようである。</s> <font color="red">うまくいかない。フルGCを行う以外に方法がないと思われる</font>

/* 3 */
public class GCTest {
  byte[]a = new byte[3000000];  
  @Override protected void finalize() throws Throwable { //!!!
    super.finalize(); //!!!
  }  //!!!
  public static void main(String[]args) {
    while (true) {
      new Thread() {
        public void run() {          
          GCTest test = new GCTest();
          System.out.println("CREATED");
        }
      }.start();
      System.gc(); //!!!
    }
  }
}

あるいは、(時間的に余裕があるならば)以下のようにしてもよい。

/* 4 */
public class GCTest {
  byte[]a = new byte[3000000];  
  @Override protected void finalize() throws Throwable { //!!!
    super.finalize(); //!!!
  }   //!!!
  public static void main(String[]args) {
    new Thread() { //!!!
      public void run() { //!!!
        try { //!!!
          Thread.sleep(1000); //!!!
        } catch (Exception ex) {     //!!!      
        } //!!!
        System.gc(); //!!!
      } //!!!
    }.start(); //!!!
    while (true) {
      new Thread() {
        public void run() {          
          GCTest test = new GCTest();
          System.out.println("CREATED");
        }
      }.start();
    }
  }
}

GCの挙動を観察してみると、/* 2 */のコードでも自動的にフルGCは行われているが、使用ヒープ領域は減少していない。Finalizerで埋め尽くされてしまった後にフルGCを行っても意味が無いようである。

  • finalize()を実装したオブジェクトを使用する場合(特に複数のスレッドで生成する場合)には、明示的にフルGCを指示しなければならない模様。ヒープ領域が満杯になった際の自動フルGCでは回収されないようである。

参考資料

RMIを使用している場合

RMIを使用する場合は、デフォルトで1分間に一度フルGCが指示されているようである。 したがって、RMIを使用するシステムと、使用していないシステムでは、それ以外の部分で同じ処理を行っていても挙動が異なる。

フルGCによって弱参照オブジェクトはどうなるか?

SoftReferenceは保持される。

public class GCTest {  
  private static SoftReference<Object> ref;
  public static void main(String[]args) {
    ref = new SoftReference<Object>(new Object());
    while (true) {
      if (ref.get() == null) {
        ref = new SoftReference<Object>(new Object());
        System.out.println("Creating");
      } else {
        System.out.println("Existing");
      }
      System.gc();
    }
  }
}

WeakReferenceは破棄される。

public class GCTest {  
  private static WeakReference<Object> ref;
  public static void main(String[]args) {
    ref = new WeakReference<Object>(new Object());
    while (true) {
      if (ref.get() == null) {
        ref = new WeakReference<Object>(new Object());
        System.out.println("Creating");
      } else {
        System.out.println("Existing");
      }
      System.gc();
    }
  }
}