Hello,

I have been investigating some memory problems we've been seeing, having embedded rhino in our application. The symptom was basically that there seemed to be a large amount of memory that wasn't being reclaimed after our rhino script completed.

Agressively paraphrased, our code looks as follows:

static Scriptable sealedSharedScope = /* set this up statically, once */
...

/** Run an ad-hoc script */
void runScript(Context cx, String script)
{
   // create a new scope to run the script in.
   // As per instructions:
// https://developer.mozilla.org/En/Rhino_documentation/Scopes_and_Contexts#Sharing_Scopes)
   Scriptable newScope = cx.newObject(sealedSharedScope);
   newScope.setPrototype(sealedSharedScope);
   newScope.setParentScope(null); // #setParentScope
// Create some arbitrary class, convert it to a JS object, and add it to the newScope.
   MyClass javaObj = new MyClass();
   Object jsObj = Context.javaToJS(javaObj, newScope);
   ScriptableObject.putProperty(newScope, "myObj", jsObj);
// run the script Object result = cx.evaluateString(newScope, script, "<MyScript>", 1, null);
}


Using hprof & the Eclipse Java Memory Analyzer, and looking at the rhino source, I discovered the following: Internally, Context.javaToJS() caches the names/types of the fields/methods of the java class that is being wrapped. This speeds up subsequent wrappings of other instances of this class. However, as I discovered, it /also/ caches the /scope/ that you pass to Context.javaToJS(). The following reference chain illustrates this:

#1 '- sharedScope org.mozilla.javascript.NativeObject @ 0x2aaab887c388
     '- associatedValues java.util.Hashtable @ 0x2aaab8929518
        '- table java.util.Hashtable$Entry[11] @ 0x2aaab8929558
           '- [7] java.util.Hashtable$Entry @ 0x2aaab89295c8
#2             '- value org.mozilla.javascript.ClassCache @ 0x2aaab8929608
                 '- classTable java.util.HashMap @ 0x2aaab8929640
                    '- table java.util.HashMap$Entry[16] @ 0x2aaab8929680
                       '- [6] java.util.HashMap$Entry @ 0x2aaab89aa110
#3 '- value org.mozilla.javascript.JavaMembers @ 0x2aaab89a37a8 '- members java.util.Hashtable @ 0x2aaab89a37f0 '- table java.util.Hashtable$Entry[23] @ 0x2aaab89a3830 '- [21] java.util.Hashtable$Entry @ 0x2aaab89a3900 #4 '- value org.mozilla.javascript.NativeJavaMethod @ 0x2aaab89a3930 #5 '- parentScopeObject org.mozilla.javascript.NativeObject @ 0x2aaab8929a08 #1 My static, shared, sealed scope. #2 The instance of ClassCache (which gets associated with the static, shared, sealed scope).
#3 The JavaMembers instance which caches MyClass.
#4 One of the NativeJavaMethod instances. One is created for each method/field of MyClass. #5 parentScopeObject is a referece to the scope that was passed in to Context.javaToJS().

Because of this reference chain, any properties of newScope will be inellible for garbage-collection, even after the script has run, because newScope continues to be referenced by the instance of ClassCache (the field that holds the actual reference is NativeJavaMethod.parentScopeObject).
For example, if the script that I run looks like this:
 var arr = new Array();
 for (var i=0;i<10000;i++) {
   arr.push(new MyObject());
 }
then "arr" (and all its contents) will persist in memory /after/ the script has finished running. In my particular case, I was losing about 100MB of RAM to this cache.

I'm not sure what change (if any) might be made to fix this. The method where the instance of JavaMembers is actually built: JavaMembers.lookupClass(Scriptable scope, Class dynamicType, Class staticType, boolean includeProtected) does /try/ to use the "top level" scope but it is essentially thwarted by the following line (from #setParentScope, above).
   newScope.setParentScope(null); // #setParentScope
i.e. JavaMembers.lookupClass() can't work out that it needs to use sharedSealedScope, because newScope has set its parentScope to null.

In my particular situation, I have made a simple modification to fix my particular problem. I changed the following line in my code:
   Object jsObj = Context.javaToJS(javaObj, newScope);
to
   Object jsObj = Context.javaToJS(javaObj, sharedSealedScope);
It doesn't matter to me if sharedSealedScope is held onto by ClassCache because ... it's static - i.e. I'm caching it /anyway/. This solution might not be suitable in all circumstances. For example, you might not have sharedSealedScope to-hand. Additionally, you might want to periodically set sharedSealedScope to a new value (which would /not/ remove the value cached by ClassCache).

Another option I had was to manually clear the ClassCache, but I was reluctant to do this for fear of introducing a performance cost.

I hope the above proves useful to others!

Kieran

_______________________________________________
dev-tech-js-engine-rhino mailing list
[email protected]
https://lists.mozilla.org/listinfo/dev-tech-js-engine-rhino

Reply via email to