Webエンジニア susumuis の技術ブログ

このブログの内容は個人の見解であり、所属する組織の公式見解ではありません

java.lang.reflect.Proxyは使える子か?

ResultSetみたいな巨大なインターフェースを独自で実装使用とすると、あまりのメソッドの量に途方にくれてしまいます。ちなみに僕のMacBookAir 11inch 2010年型 (メモリ4G) では、テキストエディタでひらいてもEclipseがカクカクします。

一部のメソッドを書き換えてその他多くのメソッドは標準のままに、それも、アプリケーション側のコードからはシームレスに行いたい場合、AOPを使うのが簡単です。

JavaでAOPを行いたい場合、AspectJを使うか、Javassistなどのようなバイトコードエンハンスメントライブラリを使用するのが一般的です。

しかし、Java標準APIで用意されているjava.lang.reflect.Proxyを使うこともできます。

詳細はこちらの記事などが参考になります。
最もシンプルにJavaのAOPを書いてみる→そしてJavaScriptへ at HouseTect, JavaScriptな情報をあなたに

例えば、処理を変えたい部分だけをhelperクラスに実装し、helperクラスにメソッドが存在したらそっちを、存在しなかったら、Exceptionを出すなどは以下のようにすると簡単にできます。

(ResultSet) Proxy.newProxyInstance(
	ResultSet.class.getClassLoader(),
	new Class<?>[] { ResultSet.class },
	new InvocationHandler() {
		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			try {
				Method helperMethod = helper.getClass().getMethod(method.getName(), method.getParameterTypes());
				return helperMethod.invoke(helper, args);
			} catch (NoSuchMethodException e) {
				throw new RuntimeException("This Method is not implemented");
			}
		}
	}
)

ちょっと型安全なJavaっぽくないですが、クラスを直接いじる世界に入るとそんなモノです。怖いことはありません。

しかし、心配になるのは、この処理はコンパイルを伴うので遅いのではないかということと、新たなクラスをロードするので、メモリ大丈夫か?ということです。

試しに以下のような検証プログラムを作ってみました。
ResultSetのキャッシュを作ることを想定しました。キャッシュなら、100万回くらい生成されるんじゃないかと思います。

import java.lang.management.ClassLoadingMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryUsage;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class ManyProxyMaker {
	public static void main(String[] args) throws SQLException {
		showMemoryStatus();
		List<ResultSet> list = new ArrayList<ResultSet>();
		for (int i = 0; i < 1000000; i++) {
			list.add((ResultSet) Proxy.newProxyInstance(
					ResultSet.class.getClassLoader(),
					new Class<?>[] { ResultSet.class },
					new InvocationHandler() {
						@Override
						public Object invoke(Object proxy, Method method,
								Object[] args) throws Throwable {
							try {
								Method helperMethod = ManyProxyMaker.class
										.getMethod(method.getName(),
												method.getParameterTypes());
								return helperMethod.invoke(null, args);
							} catch (NoSuchMethodException e) {
								throw new RuntimeException(
										"This Method is not implemented");
							}
						}
					}));
		}
		showMemoryStatus();
		for (ResultSet rs : list) {
			rs.getInt(1);
		}
		list.clear();
		System.gc();
		showMemoryStatus();
	}

	public static int getInt(int columnIndex) {
		return 0;
	}

	public static void showMemoryStatus() {
		MemoryMXBean memMxBean = ManagementFactory.getMemoryMXBean();
		ClassLoadingMXBean clMxBean = ManagementFactory.getClassLoadingMXBean();
		String permGen = "";
		for (MemoryPoolMXBean mxBean : ManagementFactory.getMemoryPoolMXBeans()) {
			if (mxBean.getName().equals("PS Perm Gen")) {
				permGen = getUsedMB(mxBean.getUsage());
			}
		}
		String line = getUsedMB(memMxBean.getHeapMemoryUsage()) + "\t"
				+ getUsedMB(memMxBean.getNonHeapMemoryUsage()) + "\t" + permGen
				+ "\t" + clMxBean.getLoadedClassCount() + "\t"
				+ clMxBean.getUnloadedClassCount();

		System.out.println(line);
	}

	private static String getUsedMB(MemoryUsage usage) {
		long deciMegaByte = usage.getUsed() / 1024 / 104;
		return deciMegaByte / 10 + "." + deciMegaByte % 10;
	}
}

実行結果

1.6 5.2 605 0
62.1 6.0 672 0
4.5 6.1 673 0
ERROR: JDWP Unable to get JNI 1.2 environment, jvm->GetEnv() return code = -2
JDWP exit error AGENT_ERROR_NO_JNI_ENV(183): [../../../src/share/back/util.c:820]

getIntの実装にシスプリhello, worldとかやるとちゃんと100万回ハローワールドしてくれるので、ちゃんと意図した動作はしているようです!

一時的にヒープを60MBも消費し、gcで開放されるようです。
最後のほうでなんかヤバいエラーが発生しているのも気になりますね。なんですか?!

ということで、僕の中ではこの方法は、少なくともキャッシュには使うべきではないという結論になりました。僕の検証の誤りや、うまい書き方などがあったら指摘いただけるとありがたく思います。

これが使えないとなると、一つ一つ律儀にメソッドを記述するか、javassistを使うか、APTを使うのが良いかなと思います。

個人的には、javassistはともかく、APTは未経験なので。。。うーorz

追記(17:16)

OpenJDKのソースを読んでみると、Proxyクラスは、インターフェースごとにキャッシュされていて、InvokationHandlerをコンストラクターのパラメータにしているようです。意外とシンプルな作りなんですね!

となると、65MBのヒープというのは、単にProxyクラス自体を100万個も格納したことによるインスタンスの容量で、クラスはヒープを圧迫しないようです。

エラーの原因はなんでしょう。もう一度実行したら出なかったので、MacのJDKに何かバグでもあったのでしょうか?あるいはMacの故障か?メモリとか?高速にメモリ割り当て・開放を行っていることが原因かもしれないですね。

追記(17:24)

こんなコードを書いてみた。

	public static void main(String[] args) throws SQLException {
		showMemoryStatus();
		List<Object> list = new ArrayList<Object>();
		for (int i = 0; i < 1000000; i++) {
			list.add(new Object());
		}
		showMemoryStatus();
		// for (ResultSet rs : list) {
		// rs.getInt(1);
		// }
		list.clear();
		System.gc();
		showMemoryStatus();
	}

結果

1.6 5.2 604 0
25.0 5.2 605 0
4.3 5.3 605 0

なんだ、単なるObjectでも100万個もインスタンス作ったら25MBも使うじゃないか!Proxyクラスの方がでかいのはしょうがないのだから、結論としては、Proxyクラス悪い子じゃなかった!

あと、僕のMac大丈夫か?

追記(20:10)

百万=1M個もあるんだから、消費メモリもメガ単位なのは当たり前というアタリマエな指摘をいただきました。

Java.lang.Objectのサイズは8バイトらしいです。
オブジェクトサイズを測る(1) - #35