Javassist、ProxyFactoryのキャッシュ戦略
cglibにしろJavassistにしろオンザフライで拡張した型を際限無く生成していれば、あっという間にリソースの上限を超えてしまう。普通、生成したクラスの情報はPerm領域に積まれるが、この領域は拡張されていないことが多いためにOutofMemoryを起こしてしまうことが多い。そのため、両ライブラリィともに一定のルールで生成した型をキャッシュしており、必要の無い型の生成を抑制している。※
JavassistのProxyFactoryも型を生成するためのキャッシュ機構を持っているが、その戦略は木状になっており以下の階層順に格納されている。
1.クラスローダ
トップレベルのキャッシュはHashMapであり、内部で取得したクラスローダが違うと判断された場合、2.のキャッシュキーが同一でも違う値と判定される。(クラスローダの取得方法はスタティックフィールドを使ってカスタマイズできる)
2.キャッシュキー
クラスローダ階層下は以下のオブジェクトで複合キークラスを構成し、このキーが同じと認識された場合には内部に格納されている生成済みの型を返す。
Class(元の型), Class[](インタフェース), MethodFilter, MethodHandler
実際には型名を文字列に変換した値のハッシュと、与えられたMethodFilter, MethodHandlerが同値かをオーバライドされたequalsメソッドで判断しており、以上が全て同じキーが存在する場合は新たに型を生成しない。
先日書いたProxyFactoryの例は、このキャッシュ機構から見ると問題があることが判る。
//ProxyFactoryはデフォルトでキャッシュ機構が有効になっている ProxyFactory factory = new ProxyFactory(); factory.setSuperclass(Hoge.class); factory.setFilter(new MethodFilter(){ @Override public boolean isHandled(Method method) { return !(method != null && method.getName().equals("equals") && method.getReturnType() == boolean.class && method.getParameterTypes().length == 1 && method.getParameterTypes()[0] == Object.class); }}); factory.setHandler(new MethodHandler(){ @Override public Object invoke(Object self, Method method, Method proceed, Object[] args) throws Throwable { System.out.println("*** before " + method.getName() + " ***"); proceed.invoke(self, args); System.out.println("*** after " + method.getName() + " ***"); }}); Class enhancedClass = factory.createClass();
既に種明かしをしたので解説はしないが、このロジックを複数回通すと、最後の行で生成されるenhancedClassには全て違うクラスが生成されて代入される。
ここから解るのは、キャッシュが使えるようになっているのが解っていても、その戦略を知らないと有効に利用できないということだ。
マップの実装によってはキャッシュされている内容がGCの対象にならない(今回の場合はWeakHashMapが使われていた)場合もあり、"キャッシュ機能"という触れ込みを見たら実装の詳細を(可能であれば)確認すべきだろう。
※元になっているのはjava.lang.reflect.Proxyクラスなのだし、同様のキャッシュ機構は持っていて然りか。(java.lang.reflect.Proxyクラスのキャッシュは与えられたインタフェース名が格納された文字列配列から導出される)