The King's Museum

ソフトウェアエンジニアのブログ。

【Effective Java】項目75:カスタムシリアライズ形式の使用を検討する(後半)

前回の記事ではデフォルトシリアライズとカスタムシリアライズの概要について説明しました。

hjm333.hatenablog.com

本記事では、カスタムシリアライズ実装の注意事項について説明します。

StringList のカスタムシリアライズ実装を再掲します。

public class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }

    /**
     * この{@code StringList}インスタンスをシリアライズする。
     *
     * @serialData リストのサイズ({@code int})を書き出して、
     * 適切な順番にすべての要素({@code String})が続くようにする
     */
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);
        for (Entry e = head; e != null; e = e.next) {
            s.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int num = s.readInt();
        for (int i = 0; i < num; i++) {
            add((String) s.readObject());
        }

    }
}

注意点

transient フィールド

StringList のカスタムシリアライズ実装では private フィールドに transient 修飾子が指定されています。

transient 修飾子は、デフォルトのシリアライズ対象からそのフィールドを除外することを指定します。 すなわち、カスタムシリアライズ実装では StringList のすべての private フィールドは自動的にはシリアライズされなくなります。

デフォルトシリアライズを利用するかどうかに関わらず、defaultWriteObject を呼び出すと transient 指定されたフィールド以外のフィールドはすべてシリアライズされます。 無用なフィールドのシリアライズを避けるために、可能な限りフィールドは transient 指定するべきです。

特に transient を指定するべきフィールドとして、

  • データフィールドから値を計算できるフィールド(キャッシュなど)
  • ネイティブのデータ構造へのポインタを表すフィールド
  • 特定の JVM の1回の実行にひもづくフィールド

があります。

基本的にすべてのフィールドを transient 指定しようと考えてから、『このフィールドは transient を外そう』と考えるようにします。 この時、本当に transient でなくてよいかはよく確認してください。

transient フィールドのデフォルト値

transient 修飾子を指定した場合、デシリアライズされた際のフィールド値は、その型のデフォルト値になります。 オブジェクトならば null、boolean なら false、int ならば 0 です。

これらの値が受け入れられない場合、値を適切に設定する readObject メソッドを実装しなければなりません。 デシリアライズは言語外の仕組みを使うので、コンストラクタによる不変式の強制は利用できません。

defaultReadObject

StringList のフィールドはすべて transient 修飾子が指定されています。

すなわち、デフォルトシリアライズシリアライズされるフィールドはないはずです。 それにも関わらず writeObject 内で defaultWriteObject() を呼び出し、さらに readObject で defultReadObject() を呼んでいることに注意していください。

この呼び出しをすることで、後のリリースで StringList に transient ではないフィールドを追加することを可能にします。

もし、新しいバージョンで transient ではないプロパティが追加されて古いバージョンでデシリアライズした際、仮に defaultReadObject() を呼んでいないとしたら StreamCorruptedException が発生してしまいます。

@serialData

writeObject() は private メソッドにも関わらず、ドキュメンテーションコメントがつけられていることに注意してください。

writeObject() はシリアライズ形式を示しています。そして、シリアライズ形式は公開 API の一部です。 そのため、@serialData タグを用いてシリアライズされるデータの定義を書かなければなりません。

シリアライズが不適切な場合

前回述べたように StringList をデフォルトシリアライズにすることはいくつかの点で不適切です。 しかし、StringList のデフォルトシリアライズは『元のオブジェクトを正しくシリアライズし、デシリアライズによって正しく復元できる』という意味では正しいものです。

一方、デフォルトシリアライズを選ぶと、明確にエラーを引き起こすクラスがあります。

たとえばハッシュテーブルでは、デフォルトのシリアライズは機能しません。 なぜなら、キーとなるハッシュ値が、異なる JVM 間では違う可能性があります。 さらにいうとプログラムの実行ごとにハッシュ値が異なる可能性すらあります。

そのため、ハッシュテーブルにおけるデフォルトシリアライズによってハッシュ値シリアライズされると、デシリアライズした際に不変式が破られている可能性があります。

synchronized

もし、オブジェクトが同期によってスレッドセーフを実現しているならば、 writeObject メソッドは synchronized にする必要があります。

シリアルバージョン UID

Serializable を実装する場合、すべてのクラスで明示的なシリアルバージョン UID を定義してください。

シリアルバージョン UID は次のように宣言します。

private static final long serialVersionUID = randomLongValue;

明示的にシリアルバージョン UID を宣言すると、クラスのシリアライズに関する互換性を保つことができます。 これを宣言しない場合、シリアルバージョン UID はクラスの名前やフィールドを元にして自動生成されます。 すなわち、フィールド一つ追加しただけで、シリアルバージョン UID が変わってしまい、互換性が失われてしまいます。

また、明示的にシリアルバージョン UID を宣言しないと、実行時に UID を生成するためコストの高い計算が必要となります。

randomLongValue の値はどのような値でも問題ありませんが、serialvar ユティリティを実行すると値を生成できます。 最近の IDE には seiralVersionUID 生成機能が含まれています。

もし、クラスの古いバージョンにシリアルバージョン UID がついていない場合、新しいバージョンには古いバージョンのシリアルバージョン UID 自分で計算して、付け加える必要があります。

もし、異なるシリアルバージョン UID を持つクラスをデシリアライズしようとすると InvalidClassException が発生します。

【Effective Java】項目75:カスタムシリアライズ形式の使用を検討する(前半)

シリアライズする方法には次の二つの種類があります。

何の考慮もせずにデフォルトシリアライズ形式を用いることは、互換性やパフォーマンスの点で非常に危険です。 なるべくカスタムシリアライズ形式を用いるべきです。

デフォルトシリアライズ形式

デフォルトシリアライズを利用するには単に Serializable を実装します。 あとは何もしなくても、フレームワークが自動的にそのクラスの内部表現を効率的に符号化します。

すなわち、デフォルトシリアライズではクラスに含まれるすべてのプロパティが自動的にシリアライズされます。 そのため、本来はシリアライズするべきではない、実装上の都合やキャッシュなどのプロパティをすべてシリアライズしてしまいます。

たとえば、名前と年齢を持つ人物を表すクラスを考えてみます。 このクラスでデフォルトシリアライズを利用するには、単に次のように Serializable を実装するだけです。

public class Person implements Serializable {
    /**
     * 名前。非 null。
     * @serial
     */
    private String name;
    /**
     * 年齢。0以上。
     * @serial
     */
    private int age;

    public Person(String name, int age) {
        if (name == null) {
            throw new IllegalArgumentException("name = null");
        }
        if (age < 0) {
            throw new IllegalArgumentException("age: " + age + " < 0");
        }

        this.name = name;
        this.age = age;
    }
}

ここで、シリアライズに関するいくつか注意点があります。

@serial タグ

name と age のフィールドは private ですが Javadocドキュメンテーションコメントが含まれています。 private プロパヒットであっても、シリアライズされてしまえば公開された API として扱うため、ドキュメンテーションコメントをつけるべきです。

@serial タグを使えば Javadoc が「シリアライズ形式の文書化」に関する特別ページに、そのプロパティを掲載してくれます。

readObject メソッド

デフォルトシリアライズでは readObject メソッドの実装は必須ではありません。

しかし、不変式とセキュリティを保証するために readObject メソッドは必ず実装するべきです。

たとえば、Person では name と age には次の不変式があります。

  • name は null
  • age は 0 以上

もし、シリアライズされたファイルを操作して、これらの不変式を破壊するようなデータが仕込まれた場合、readObject がなければそのままデシリアライズされてしまうからです。 これに関する詳細については項目76で扱います。

カスタムシリアライズ形式

カスタムシリアライズを用いるためには Serializable を実装することに加えて readObject メソッドと writeObject メソッドを実装します。

readObject メソッドと writeObject メソッドで、開発者がシリアライズするべきプロパティを選びます。

これによって、単にクラスの物理表現(実装)がシリアライズするのではなく、オブジェクトの論理表現(仕様)をシリアライズすることができるようになります。

たとえば文字列のリスト示すクラスの StringList があるとします(これはあくまで例なので通常は List を使うべきです)。

public class StringList implements Serializable {
    private int size = 0;
    private Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }
    ...(省略)....
}

物理表現と論理表現

このクラスは論理的に表現すれば「文字列のリスト」ですが、物理的(実装的)に表現すると「文字列の双方向リンクリスト」と考えられます。 そして、デフォルトシリアライズを用いると物理表現である「文字列の双方リンクリスト」としてシリアライズされてしまいます。

これには次の4つの欠点があります。

  • 物理表現(実装)が公開 API となり、永久にサポートする必要がある
  • 多くの空間を消費する可能性がある
  • 多くの時間を消費する可能性がある
  • スタックオーバーフローを起こす可能性がある

一方、カスタムシリアライズを利用してクラスの論理表現をシリアライズすれば、これらの問題を回避し、よりシンプルにシリアライズすることができます。 論理的には StringList は論理的「リスト中の文字列数」と「文字列自身」を記憶しておけば十分だからです。

カスタムシリアライズを実装した StringList は次のようになります。

public class StringList implements Serializable {
    private transient int size = 0;
    private transient Entry head = null;

    private static class Entry implements Serializable {
        String data;
        Entry next;
        Entry previous;
    }

    /**
     * この{@code StringList}インスタンスをシリアライズする。
     *
     * @serialData リストのサイズ({@code int})を書き出して、
     * 適切な順番にすべての要素({@code String})が続くようにする
     */
    private void writeObject(ObjectOutputStream s) throws IOException {
        s.defaultWriteObject();
        s.writeInt(size);
        for (Entry e = head; e != null; e = e.next) {
            s.writeObject(e.data);
        }
    }

    private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
        s.defaultReadObject();
        int num = s.readInt();
        for (int i = 0; i < num; i++) {
            add((String) s.readObject());
        }

    }
}

このカスタムシリアライズの実装にはいくつか注意点があります。 次回の記事でその注意点について述べます。

感想

長いので分割。。。 これを含めてあと4つか~。

【Effective Java】項目74:Serializable を注意して実装する

項目74からはオブジェクトシリアライズ API について説明します

オブジェクトをバイト列として符号化することをシリアライズバイト列からオブジェクトに復号化することをデシリアライズと呼びます。

シリアライズされたオブジェクトは仮想マシン間やネットワークをまたいで転送できるようになります。 また、ファイルに保存できるようになります。

Serializable

Java でクラスをシリアライズ可能にするには Serializable インタフェースを implements します。

一般的に、コレクションクラスや値クラスは Serializable を実装しても問題ありません。 しかし、スレッドプールなどの処理をまとめたクラスは Serializable を実装するべきではありません。

やるべき作業は単に Serializable インタフェースを implements するだけですが、その簡単さとは裏腹にいくつか考慮しなければならない点があります。

柔軟性の低下

Serializable を実装したクラスを一旦リリースしてしまうと、その実装を変更することは容易ではありません。

クラスをシリアライズ可能にすると、そのシリアライズ形式がクラスの公開 API の一部となります。 既存のシリアライズ形式を永久にサポートし続ける必要がでてきます。

カスタムシリアライズ形式を設計せずにデフォルトのシリアライズ形式を利用すると、シリアライズ形式はクラスの内部表現と結びついてしまいます。 すなわち、デフォルトのシリアライズ形式では private インスタンスフィールドでさえも公開されてしまうことになります。

最も単純な問題例はシリアルバージョン UID と呼ばれるストリーム一意識別子に関する問題です。 Serializable を implements したクラスは必ずこの識別子を保持するようになります。 これを明示的に指定しない場合には次の情報からコンパイラが自動生成します。

  • クラス名
  • クラスが実装しているインタフェース名
  • public と protected のメンバ

もし、識別子を明示的に指定しない場合、単に便利なメソッドを一つ追加しただけでこの識別子は変更されてしまいます。 結果として、互換性が破壊されてしまいます。

セキュリティ

二つ目に、シリアライズ可能にすることでバグやセキュリティホールが入り込む可能性を増大させるという問題があります。

シリアライズ時のオブジェクトは言語外の仕組みを使い、通常のコンストラクタを迂回して生成されます。 すなわち、本来のコンストラクタによって保証される不変式がオブジェクト生成時には保証されません。

そのため、まずはデシリアライズ処理時に不変式を満たすように正しく実装する必要があります。 また、不変式を満たさない生成中のオブジェクト内部に外部からアクセスができないようにしなければなりません。

テスト負荷の増大

三つ目に、シリアライズ可能なクラスでは変更時のテストの工数が通常よりも増えます。

新たなリリースのインスタンスシリアライズし、古いリリースのデシリアライズ処理でインスタンスが復元できるを確認する必要があります。 また、逆に、古いリリースのインスタンスシリアライズし、新しいリリースのデシリアライズ処理で復元できるかも確認する必要があります。

これらのテストは単に新旧のインスタンス間でシリアライズ/デシリアライズできるかというバイナリ互換性に加えて、 動作が意図しているものかどうかというセマンティクス互換性も検査する必要があります。

これの負荷はカスタムシリアライズ形式を正しく設計することで軽減できますが、それを完全になくすことはできません。

継承と Serializable

継承するために設計されたクラス(項目17)とインタフェースでは Serializable を拡張するべきではありません。

継承して利用するために設計されたクラスが Serializable を implements している場合、サブクラスの実装にかなりの手間がかかります。

しかし、継承のために設計されたクラスがシリアライズ可能でない場合、シリアライズ可能なサブクラスを書くことは不可能かもしれません。 シリアライズ可能なサブクラスを書くためには、親クラスにアクセス可能なパラメータ無しコンストラクタが必要です。

内部クラスと static メンバークラス

内部クラスは Serializable を実装するべきではありません。 内部クラスはそれを包含するエンクロージングインスタンスに対する参照や、外部スコープからのローカル変数の値を保持するためのコンパイラが生成するフィールドを使用しています。 これらに対するデフォルトのシリアライズ形式は未定義で不明瞭で、動作は保証されません。

一方、static メンバークラスはエンクロージングインスタンスに対する参照などを持っていません。 そのため、自由に Serializable を実装できます。

【Effective Java】項目73:スレッドグループを避ける

スレッドグループはもはや利用するべきではありません。

スレッドグループのスレッド一覧を取得するための enumerate メソッドは、スレッドを格納する十分な大きさの配列が渡されない場合、勝手に一部のスレッドを無視します。 また、スレッドの数を取得する activeCount メソッドは、それを呼び出した後も正しいスレッド数を表しているとは限りません。

このように、スレッドグループの API が提供する多くの機能には欠陥があります。 もし、スレッドをまとめて管理したい場合には java.util.concurrent のエグゼキューターフレームワークを使うべきです。

setUncaughtExceptionHandler

スレッドグループのみが提供していた機能に「キャッチされない例外に対する制御を指定する」という機能があります。ThreadGroup.uncaughtException です。

Java 5 以降は Thread に setUncaughtExceptionHandler が追加されているため、こちらを使うべきです。

これで完全に ThreadGroup を使う必要はなくなりました。

【Effective Java】項目72:スレッドスケジューラに依存しない

プログラムの正しさやパフォーマンスをスレッドスケジューラーの動作に依存させてはいけません。

実行可能なスレッド数はプロセッサの数と比べて非常に大きくならないようにするべきです。 そうしておけば、スレッドスケジューラーがどのようなアルゴリズムを採用していてもプログラムは正しく動作します。

実行可能なスレッド数を少なくするためにはスレッド数を適切に制限するべきです。 エグゼキューターフレームワーク項目68)では、スレッド数を制限したスレッドプールを作成できます。

ビジーウェイト

特定の条件を満たすまでスレッドを待機させるためにビジーウェイトを使うべきではありません。 ビジーウェイトはプログラムを脆弱にし、プロセッサへの不可が増大します。

例えば次のようなビジーウェイトを用いた CountDownLatch はとても遅いです。

public class SlowCountdownLatch {
    private int count;

    public SlowCountdownLatch(int count) {
        if (count < 0) {
            throw new IllegalArgumentException(count + " < 0");
        }
        this.count = count;
    }

    public void await() {
        while (true) {
            synchronized (this) {
                if (count == 0) {
                    return;
                }
            }
        }
    }

    public synchronized void countDown() {
        if (count != 0) {
            count--;
        }
    }

}

Thread.yield()

Thread.yield() は他のスレッドに処理を実行させる機会を与えます。 しかし、機会を与えるだけで実際に他のスレッドの処理が実行されるとは限りません。

ある JVM 実装でパフォーマンスが改善する yield() 呼び出しが、他の実装では動作しないかもしれません。 実際、ある JVM 実装において yield() は何もしません。

Thread.yield() を利用する唯一の箇所はテストでした。 テストで yield を使うと実行パターンの状態空間の数が増え、バグをあぶり出すことができました。

しかし、前述したように JVM 実装によっては挙動が異なるため、普通は Thread.sleep(1) を使うべきです。 Thread.sleep(0) は実装によっては何もしないので、Thread.sleep(1) を使うべきです。

【Effective Java】項目71:遅延初期化を注意して使用する

遅延初期化(lazy initialization)は、フィールドの値が必要になるまで初期化を遅らせる手法です。 static フィールドとインスタンスフィールドの両方に利用できます。

まず、第一に、通常は遅延初期化を使うべきではありません。 初期化を遅らせる仕組みによって、フィールドへのアクセスコストが増加するからです。

遅延初期化を採用する理由は間違いなく「最適化」が目的ですが、項目55で述べたように多くの「最適化」は失敗します。 パフォーマンスを測定し、ほんとうに遅延初期化が必要かどうかを必ず判断してください。

マルチスレッド環境の遅延初期化

通常の初期化

遅延初期化しない通常の初期化は次のように行われます。

private final FieldType field = initializeField();

これは static フィールドの場合も同様です。 final キーワードがついていることが重要です。

final キーワードはマルチスレッド環境においても安全にフィールドが初期化されることを保証するからです。

通常の遅延初期化

次に最も一般的な遅延初期化の方法を紹介します。

private FieldType fieldType;
public synchronized FieldType getField() {
    if (field == null) {
        field = initializeField();
    }
    return field;
}

これは static フィールドでも、インスタンスフィールドでも有効です。 synchronized キーワードによって適切に同期化されていることが重要です。

最適化された遅延初期化

もし、単に同期化しただけの遅延初期化でパフォーマンス上の問題が発生するならば、いくつかの選択肢があります。

遅延初期化ホルダークラス

static フィールドの初期化には遅延初期化ホルダークラスイディオムを使うと効果的です。

private static class FieldHolder {
    static final FieldType field = initializeField();
}

static public FieldType getField() {
    return FieldHolder.field;
}

これは『クラスが利用されるまでクラスが初期化されない』という言語仕様を利用したイディオムです。

ダブルチェックイディオム

遅延初期化ホルダークラスはシンプルで効果的な実装ですが、インスタンスフィールドには適用できません。

インスタンスフィールドにはダブルチェックイディオムと呼ばれる遅延初期化が有効です。

private volatile FieldType field;

public FieldType getField() {
    FieldType result = field;
    if (result == null) {
        synchronized(this) {
            result = field;
            if (result == null) {
                field = result = initializeField();
            }
        }
    }
    return result;
}

synchronized で同期化する前に初期化されているかどうかをチェックし、この時点で初期化されていたらそのまま result を返します。 こうすることでほとんどの場合、同期化を避けて値を返すことが可能です。

もし、初期化されていない場合には念のために synchronized を使って同期化し、もう一度チェックします。 このように二重にチェックするためにダブルチェックイディオムと呼ばれています。

フィールドが volatile であることは非常に重要です。 また、一度、ローカル変数に代入することでパフォーマンスが改善します。

このイディオムは Java 5 未満のメモリセマンティクスでは動作が保証されないことに注意が必要です。

hjm333.hatenablog.com

単一チェックイディオム

もし、特に高いパフォーマンスが求められる場合で、二回以上初期化しても問題ないのであれば synchronized を外してもかまいません。

private volatile FieldType field;

public FieldType getField() {
    FieldType result = field;
    if (result == null) {
        field = result = initializeField();
    }
    return result;
}

きわどい単一チェックイディオム

もし、long と double 以外の基本データ型であれば、単一チェックイディオムから volatile を取り除いてもかまいません。

private int field;

public int getField() {
    int result = field;
    if (result == null) {
        field = result = initializeField();
    }
    return result;
}

この手法では、新しいスレッドがアクセスするたびに initializeField() が呼ばれる可能性があります。 スレッドは、volatile 修飾子がなくても自分のスレッドが行った変数の変更は見ることができる一方、他のスレッドの変更は見ることができない可能性があるからです。

これはかなりきわどい手法ですが意図通りに動作します。 実際、String インスタンスで hashCode 値をキャッシュするために利用されています。

【Effective Java】項目70:スレッド安全性を文書化する

開発者は、クラスがマルチスレッド環境で利用された場合にどのように振る舞うかを文書化しなければなりません。

マルチスレッド環境における振る舞いを文書化しない場合、クラスの利用者は十分な同期を行わない(項目66)かもしれませんし、逆に過度な同期を行う(項目67)かもしれません。

synchronized 修飾子を手がかりにしてクラスのスレッドセーフ性を確認することは大きな誤りです。 synchronized 修飾子は API の仕様ではなく実装の詳細だからです。

synchronized 修飾子があるからといってスレッドセーフは保証されませんし、synchronized 修飾子がなくてもスレッドセーフ性が保証される場合もあります。

スレッドセーフレベル

スレッドセーフには次のようなレベルがあります。

  • 不変(immutable)
  • 無条件スレッドセーフ(unconditionally thread-safe)
  • 条件付きスレッドセーフ(conditionally thread-safe)
  • スレッドセーフでない(not thread-safe)
  • スレッド敵対(thread-hostile)

不変

スレッドセーフレベルが不変の場合、クラスのインスタンスは不変であるため、外部同期は一切必要ありません。 String, Integer, BigInteger などがその例です。

無条件スレッドセーフ

スレッドセーフレベルが無条件スレッドセーフの場合、クラスは可変です。 しかし、すべてのパブリックメソッドはスレッドセーフであることが保証されます。

クラスは適切に内部同期されているため、外部同期は一切必要ありません。 例としては、ConcurrentHashMap などです。

無条件スレッドセーフを実現したい場合、プライベートロックイディオムを使うことが効果的です。

private final Object lock = new Object();
public void foo() {
    synchronized(lock) {
        ....
    }
}

クライアントがロックオブジェクトに触ることができると、悪意あるクライアントによってサービス拒否攻撃が行われる可能性があります。 このイディオムを用いれば、外部からロックオブジェクトに触ることが出来なくなり、この攻撃を防ぐことができます。

また、プライベートロックイディオムは継承のために設計されたクラスに対して有効です。

条件付きスレッドセーフ

このレベルの場合、クラスは可変であり、いくつかのメソッドは適切な外部同期を必要とします。 例えば Collections.synchronized ラッパーが返すコレクションです。

Collections.synchronized ラッパーが返すコレクションのイテレータを利用したい場合、次のような外部同期が必要です。

Map<K, V> map = Collections.synchronizedMap(new HashMap<K, V>()));
Set<K> set = m.keySet(); // ここでは同期は必要ない
// イテレータを利用する場合、map に対するロックが必要
synchronized (map) {
    for (K key: set) { 
        ....
    }
}

このように、条件付きスレッドセーフレベルではクライアントにロックオブジェクトを渡す必要があります。 そのため、条件付きスレッドセーフではプライベートロックイディオムを使うことはできません。

スレッドセーフでない

このレベルでは、クラスは可変ですべてのメソッドが外部同期が必要です。 例えば、ArrayList や HashMap などです。

スレッド敵対

このレベルのクラス/メソッドは、たとえ外部同期されていてもマルチスレッドで利用することは安全ではありません。

このクラスは Java ライブラリにもほとんどありません。 希有な例としては System.runFinalizersOnExit() です。

感想

System.runFinalizersOnExit() がなぜだめなのか調べた。

まずはリファレンスをチェック。

Runtime (Java Platform SE 7)

非推奨。 このメソッドは本質的に安全ではありません。ファイナライザがライブオブジェクトに対して呼び出される結果になる可能性があり、そのときにほかのスレッドがそれらのオブジェクトを並行して操作していると、動作が異常になるか、デッドロックが発生します。   終了時のファイナライズを有効または無効にします。これを実行することによって、自動的に呼び出されていないファイナライザを持つすべてのオブジェクトのファイナライザが呼び出され、Java Runtime の終了前に実行されるようになります。

なるほど、これをオンにすると終了時に各オブジェクトの finalize() メソッドが必ず呼び出されるようになる。 この時、そのオブジェクトが並行に実行されていたりする可能性があるので危険、ということだな。

(c) The King's Museum