Mayaaのソースコードで気になったところ

まだ完全に実環境で現象が再現するところまで行ってないのでメモだけ。Mayaaのバージョンはさっき落とした1.1.26。
本当はMLとかに投げるべきかもしれない。間違いがあれば指摘をお願いします。

SpecificationNodeImplのequals()

org.seasar.mayaa.impl.engine.specification.SpecificationNodeImplのequals()の実装を見ると下のような感じ。

    public boolean equals(Object other) {
        if (other instanceof SpecificationNodeImpl) {
            SpecificationNodeImpl otherNode = (SpecificationNodeImpl) other;
            return _hashCode == otherNode._hashCode;
        }
        return false;
    }

SpecificationNodeImplまたはそのサブクラスで_hashCodeが一致すれば同じオブジェクト扱いになっている。_hashCodeはsetSequenceID()の中で更新している。

    public void setSequenceID(int sequenceID) {
        if (sequenceID < 0) {
            throw new IllegalArgumentException("sequenceID");
        }
        _sequenceID = sequenceID;
        _hashCode = (_qName.toString() + ":" + getSystemID() +
                ":" + _sequenceID + ":" + super.hashCode()).hashCode();
    }

_hashCodeへの代入式の中にsuper.hashCode()とあるが、親クラスのNamespaceImplクラスにはhashCode()は実装されてないので、ここではObjectクラスのhashCode()が呼ばれる。つまり、

  • _qName,getSystemID(), _sequenceIDの値が全て同じでも、同一(==)のオブジェクトでない限り、ほぼequals()が真にならない。
  • 逆に、それらが全て異なっていても、hashCodeが偶然同じになればequals()が真になることがある。

これはちょっと意図が分からない。


ちなみにSpecificationNodeのequals()が単なる同一のオブジェクト判定だと、次のようなことが起こる。

  • 同じsystemID(=同じmayaaファイル)の、同一でないPageImplから取得した、エレメントに対応するSpecificationNode同士がequals()にならない
  • 同じsystemID(=同じmayaaファイル)の、同一でないPageImplから取得した、エレメント内の諸々のエレメントに対応するSpecificationNode同士がequals()にならない


PageImplはEngineImplの_specCacheでキャッシュされていて、systemIDが同一であれば使い回しされるのだけど、

  • ほぼ同時に同一のmayaa/htmlにアクセスがあった場合
  • メモリ負荷が高い環境で、間隔を開けて同一のmayaa/htmlにアクセスがあった場合

には同じsystemIDに対して複数個生成されてしまう。(前者はcreatePageInstance(String)が排他制御されていない為、後者はReference Sweep Monitorがキャッシュを削除するため)


PageImplが異なれば、SpecificationNodeのhashCodeもおそらく異なるため、SpecificationNodeをMapのキーに使用している箇所ではほとんどヒットしなくなる。(まあわざとかもしれないけど。)

EventScriptEnvironmentの_mayaaScriptCache

org.seasar.mayaa.impl.engine.specification.SpecificationUtilの内部クラスのEventScriptEnvironmentはm:beforeRenderやm:afterRenderに書かれたJavaScriptコンパイル結果をキャッシュするのに使われているようだ。


以下、例としてmayaaファイル内にエレメントが宣言され、その内部にJavaScriptが書かれている場合の、キャッシュの動きについて考えてみる。


EventScriptEnvironmentクラスでは、コンパイル結果を保持するために、次のようにWeakHashMapをインスタンス化して使っている。

    protected static class EventScriptEnvironment {

        private WeakHashMap _mayaaScriptCache = new WeakHashMap();
...
        protected CompiledScript findScriptFromCache(
                SpecificationNode mayaa, Object key) {
            Map scriptMap = (Map) _mayaaScriptCache.get(mayaa);
            if (scriptMap != null) {
                return (CompiledScript) scriptMap.get(key);
            }
            return null;
        }


_mayaaScriptCacheの型は、総称型で書けば

    WeakHashMap<SpecificationNode, Map<Object, CompiledScript>>

になる。ここでObject型のkeyは実際にはSpecificationNode(例えばm:beforeRenderエレメントに対応する)であり、scriptMapはHashMapなので、それを加味すれば、

    WeakHashMap<SpecificationNode, HashMap<SpecificationNode, CompiledScript>>

となる。


_mayaaScriptCacheの値の方の生成箇所を見てみると、以下のようにHashMapを生成し、mayaa(m:mayaaエレメントに対応するSpecificationNode)をキーにして_mayaaScriptCacheにputしている。そのHashMapには、keyをキーにして、CompiledScriptをputしている。

        protected CompiledScript newScriptFromCache(
                SpecificationNode mayaa, Object key,
                String scriptText, boolean fullScript) {

            synchronized (_mayaaScriptCache) {
                CompiledScript script = findScriptFromCache(mayaa, key);
                if (script != null) {
                    return script;
                }

                script = compile(scriptText, fullScript);
                Map scriptMap = (Map) _mayaaScriptCache.get(mayaa);
                if (scriptMap == null) {
                    scriptMap = new HashMap();
                    _mayaaScriptCache.put(mayaa, scriptMap);
                }
                scriptMap.put(key, script);

                return script;
            }
        }

入れ子状態の各要素を書くと次のようになる。

  • _mayaaScriptCache(WeakHashMap)
    • キー: mayaa(m:mayaaに対応するSpecificationNode)
    • 値: HashMap
      • キー: key(m:mayaa内のm:beforeRenderなどに対応するSpecificationNode)
      • 値: CompiledScript

WeakHashMapは(J2SEAPI javadocにもあるように)キーに対しては弱参照だが値オブジェクトは通常の強参照によって保持する。つまり、今回の例で言えば、

  • _mayaaScriptCache(WeakHashMap)
    • キー: mayaa(m:mayaaに対応するSpecificationNode)←弱参照
    • 値: HashMap ←強参照
      • キー: key(m:mayaa内のm:beforeRenderなどに対応するSpecificationNode) ←強参照
      • 値: CompiledScript ←強参照

のようになる。mayaa(m:mayaaに対応するSpecificationNode)がGCされれば、これらの値は全ていずれはGCされる。


ところが実際は、上で言うkeyからmayaaへの参照が存在するため、このWeakHashMapの中身はGCされないようだ。デバッガでkeyの内容を見ると、key→_delegateNodeTreeWalker→_parentというパスでmayaaに到達できる。


特定mayaaファイルに対するPageImplがキャッシュされ、同一のPageImplオブジェクトが使用されている間は、CompiledScriptについても同一のオブジェクトが使用されるが、PageImplが一旦Sweepされ新たなPageImplが生成されてしまうと、(キーが異なるため)CompiledScriptも新たに生成されてしまう。CompiledScriptはRhinoが生成したorg.mozilla.javascript.gen.cNNN(NNNの部分は数字)というクラスに対する参照を保持しているため、通常のヒープ領域だけでなく、PermGen spaceが徐々に消費されて行くことになる。


scriptMapも(HashMapではなく)WeakHashMapにするか、_mayaaScriptCache.put(mayaa, new WeakReference(scriptMap))のように値の方をWeakReferenceにしてやれば、PageImplがSweepされたタイミング(実際はその少し後だが)で_mayaaScriptCacheの中身がクリアされ、org.mozilla.javascript.gen.cNNNクラスがUnloadされるようになる。