今やっている案件でパフォーマンステストを実施したのですが、
50スレッドあたりからパフォーマンスが伸び悩むという現象が。
スレッドダンプを取ってみたところ、どうやら
org.springframework.beans.CachedIntrospectionResults#forClass
がボトルネックになっている模様。
早速ソースを見てみました。
なるほど、classCacheがsynchronizedされているのでここで待ちが発生しているんだろうなぁ。
public class CachedIntrospectionResults { static final Map<Class, Object> classCache = Collections.synchronizedMap(new WeakHashMap<Class, Object>()); ... static CachedIntrospectionResults forClass(Class beanClass) throws BeansException { CachedIntrospectionResults results; Object value = classCache.get(beanClass); if (value instanceof Reference) { Reference ref = (Reference) value; results = (CachedIntrospectionResults) ref.get(); } else { results = (CachedIntrospectionResults) value; } if (results == null) { // can throw BeansException results = new CachedIntrospectionResults(beanClass); // On JDK 1.5 and higher, it is almost always safe to cache the bean class... // The sole exception is a custom BeanInfo class being provided in a non-safe ClassLoader. if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) || isClassLoaderAccepted(beanClass.getClassLoader()) || !ClassUtils.isPresent(beanClass.getName() + "BeanInfo", beanClass.getClassLoader())) { classCache.put(beanClass, results); } else { if (logger.isDebugEnabled()) { logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe"); } classCache.put(beanClass, new WeakReference<CachedIntrospectionResults>(results)); } } return results; } }
AOPで華麗に解決!のはずが
問題のメソッドはstaticメソッドです。
でもって、Springは、任意の実装へ切り替える仕組みを用意してません。
こりゃどうもSpringのソースを書き換えるしかないっぽい。が…
それも嫌なのでアスペクトを当てて速度改善しようと思いました。
で、色々試してみたところ、どうやらSpring自体のクラスにはアスペクトを当てられない模様。
次のようなアスペクトを書いてみても、サックリ無視されてしまいます。
@Aspect public class CachedIntrospectionResultsAspect { private static final Map<Class<?>, CachedIntrospectionResults> secondCache = new ConcurrentHashMap<Class<?>, CachedIntrospectionResults>(); // CachedIntrospectionResults#forClassに二次キャッシュを適用する @Around("execution(* org.springframework.beans.CachedIntrospectionResults.forClass(..))") public Object forClass(ProcessingJoinPoint pjp) throws Throwable { final Class<?> key = (Class<?>) pjp.getArgs()[0]; if (!secondCache.contains(key)) { secondCache.put(key, (CachedIntrospectionResults) pjp.proceed()); } return secondCache.get(key); } }
staticメソッドだからアスペクトが当てられないのか?とも思ったのですが、
コチラによるとできるとのこと。
で、ここに書いてある通りやったつもりなのですが、やはりダメでした。
おそらくAOPを適用しようにも、AOPを使う時点で既にCachedIntrospectionResultsを利用している(クラスロードが完了している)
から無理なんでしょうねぇ。
最後の手段
最終的には、諦めてCachedIntrospectionResultsのソースを直接書き換えることに。
CachedIntrospectionResultsはおそらくSpringの創成期からある(タイムスタンプは2001年!!)クラスなので、
これから大幅に実装が変更になる可能性が低いと判断したことによる対応です。
まあ、CGLIBとかを使っても良かったんだけど…保守性に問題アリなのでやめました。
ソースはこんな感じ。
public class CachedIntrospectionResults { static final Map<Class, Object> classCache = new ConcurrentHashMap<Class, Object>(); ... static CachedIntrospectionResults forClass(Class beanClass) throws BeansException { CachedIntrospectionResults results = (CachedIntrospectionResults) classCache.get(beanClass); if (results == null) { results = new CachedIntrospectionResults(beanClass); classCache.put(beanClass, results); } return results; } }
もともとのコードの趣旨は『クラスイントロスペクションの結果を弱参照でキャッシュする』
だったのですが、
今回の案件に限って言えば、
- そんなに大量のスペースを食うわけでもないので弱参照にしなくてもよい。
- クラスを動的に入れ替えるわけでもないので、一回キャッシュしたらずっと保持してもOK。
という要件なので、かなり豪快に処理を端折りました。
結果
ボトルネックは解消されたっぽい。ヤレヤレ。