Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

非staticな内部クラスが持つ暗黙的な参照

Effective Java 第2版 (The Java Series)の項目22で言及されているように、原則として内部クラスはstaticにすることが推奨されます。主な理由は、staticでない内部クラスが暗黙的にエンクロージングクラス(トップレベルクラス)のインスタンスを参照するためです。

staticな内部クラスを持つクラスのヒープダンプ

staticな内部クラスはエンクロージングクラスのインスタンスへの参照を持ちません。
f:id:eller:20091112214354p:image

class HiddenReferenceTest {
	private static class InnerClass {}
}

非staticな内部クラスを持つクラスのヒープダンプ

一方、staticな内部クラスはエンクロージングクラスのインスタンスへの暗黙的な参照を持ちます。
f:id:eller:20091112214353p:image

class HiddenReferenceTest {
	private class InnerClass {}
}

staticな内部クラスを原則とする理由

staticな内部クラスは暗黙的な参照によってエンクロージングクラスのインスタンスフィールドを参照できるようになり、Iteratorやサブリストといった「複数のメンバーを共有すべき内部クラス」を容易に表現できます。特にプリミティブ型フィールドの共有に便利です。
しかし暗黙的な参照はヒープを若干占有するだけでなく、意外な形でガベージコレクションを妨げます。例えばList#sublist(int, int)が返すListがstaticでない内部クラスである場合、何者かがサブリストを参照している間は元となるListはGC対象になりません。サブリストが元となるListを暗黙的に参照するためです。
このように非staticな内部クラスは複雑でコード上に現れない参照関係を生みだし、システムの保守を難しくする可能性があります。内部クラスがエンクロージングクラスのインスタンスフィールドを参照する必要がある場合でも、まずはCopyOnWriteArrayListIteratorのようにコンストラクタやメソッドによってそれを受け渡す設計を検証すべきです。

検証コード

暗黙的な参照がGCを妨げることを簡単に見るための検証コードです。本来System.gc()をプログラム内で利用すべきではありませんが、ここでは検証用と割り切って使っています。

public class Test {
	/** TestインスタンスがGC対象になったか?をわかりやすくするための巨大オブジェクト */
	@SuppressWarnings("unused")
	private byte[] big = new byte[16 * 1024 * 1024];

	public static void main(String[] args) {
		test1();
		test2();
	}

	/** 非staticな内部クラスのインスタンスを参照しつづける=インスタンスが解放されない */
	private static void test1() {
		Test test = new Test();
		printUsedMemory();
		@SuppressWarnings("unused")
		Inner inner = test.inner;
		test = null;	// 参照を外しても、innerを通じて暗黙的に参照した状態が続く
		printUsedMemory();
		System.out.println("参照を外してもオブジェクトは解放されませんでした");
	}

	/** staticな内部クラスのインスタンスを参照しつづける=インスタンスが解放される */
	private static void test2() {
		Test test = new Test();
		printUsedMemory();
		@SuppressWarnings("unused")
		StaticInner staticInner = test.staticInner;
		test = null;	// test.staticInnerはTestインスタンスへの参照を持たないため、この時点でTestインスタンスは解放対象になる
		printUsedMemory();
		System.out.println("オブジェクトが解放されました");
	}

	/** 現在使用されているメモリ量をカンマ区切りで表示(検証・デバッグ用) */
	private static void printUsedMemory() {
		System.gc();
		Runtime r = Runtime.getRuntime();
		System.out.printf("used memory:%,11d[bytes]%n", r.totalMemory() - r.freeMemory());
	}

	private Inner inner = new Inner();
	private StaticInner staticInner = new StaticInner();

	/** staticでない内部クラス */
	class Inner {}
	/** staticな内部クラス */
	static class StaticInner {}
}