There is another situation where this problem can occur. The problem, stated a bit more concisely, is:
When using the default WrapFactory, ClassCache holds a reference to whatever the top level scope was at the time the class-in-question was cached. Usually, this is truly the top-level scope, which doesn't cause any problems.
However, if you're using a shared "global" scope, as per:https://developer.mozilla.org/En/Rhino_documentation/Scopes_and_Contexts#Sharing_Scopes then this sharedScope isn't used by ClassCache; it's actually your "newScope", as follows:
Scriptable sharedScope = ... create shared scope. create a scope that uses the shared scope Scriptable newScope = cx.newObject(sharedScope); newScope.setPrototype(sharedScope); newScope.setParentScope(null);then run a script in newScope, in particular one that ends up wrapping a java class, either via Context.javaToJS() or actually in your javascript. Even if you don't do this explicitly, things like throwing a java exception will end in WrapFactory being used.
After the above, you can end up with the following: sharedScope -stores-reference-to-> classCacheInstance classCacheInstance -stores-reference-to-> newScope.This means that newScope will /never /become eligible for garbage collection. If you created some large objects in newScope then these will never be GC-ed either. Hence....memory leak.
My solution was the following: Create a custom WrapFactory that always uses the shared scope:
static final WrapFactory customWrapFactory = new WrapFactory () {
public Object wrap(Context cx, Scriptable scope, Object obj,
Class staticType)
{
return super.wrap(cx, sharedScope, obj, staticType);
}
public Scriptable wrapAsJavaObject(Context cx, Scriptable scope,
Object javaObject, Class staticType)
{
return super.wrapAsJavaObject(cx, sharedScope, javaObject,
staticType);
}
public Scriptable wrapNewObject(Context cx, Scriptable scope,
Object obj)
{
return super.wrapNewObject(cx, sharedScope, obj);
}
};
And then use it to initialize you Context:
cx.setWrapFactory(customWrapFactory);
This fixed my problem. The reference chain now looks as follows:
sharedScope
-stores-reference-to-> classCacheInstance
classCacheInstance
-stores-reference-to-> /sharedScope/
which is fine, and doesn't lead to a memory leak.
Kieran
Kieran Topping wrote:
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 scriptObject 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); // #setParentScopei.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
smime.p7s
Description: S/MIME Cryptographic Signature
_______________________________________________ dev-tech-js-engine-rhino mailing list [email protected] https://lists.mozilla.org/listinfo/dev-tech-js-engine-rhino
