Skip to content

Commit 70e0c18

Browse files
committed
Document PhantomReference solution
Add extensive commenting to explain why PhantomReferences are needed to solve the memory leak identified in this implementation of the Python script language. Fixes http://fiji.sc/bugzilla/show_bug.cgi?id=1203
1 parent 1f8826d commit 70e0c18

File tree

2 files changed

+55
-4
lines changed

2 files changed

+55
-4
lines changed

src/main/java/org/scijava/plugins/scripting/jython/JythonBindings.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,22 @@ public class JythonBindings implements Bindings {
5555

5656
protected final PythonInterpreter interpreter;
5757

58+
/*
59+
* NB: In our JythonScriptLanguage we explain the need for cleaning
60+
* up after a PythonInterpreter and declare the scope of a
61+
* Python environment to be equal to the lifetime of its parent
62+
* JythonScriptEngine.
63+
* As triggering our cleaning method involves PhantomReferences
64+
* we must ensure that JythonScriptEngines can actually be
65+
* garbage collected when it is no longer in use.
66+
* To do this, we have to prevent JythonScriptEngines from
67+
* being passed to the PythonInterpreter - which would then
68+
* create a hard reference to the ScriptEngine through the
69+
* static PySystemState.
70+
* Similarly we do not want to pass ScriptModules to the
71+
* PythonInterpreter, as the ScriptModule has a hard
72+
* reference to its ScriptEngine.
73+
*/
5874
private Map<String, WeakReference<Object>> shallowMap =
5975
new HashMap<String, WeakReference<Object>>();
6076

src/main/java/org/scijava/plugins/scripting/jython/JythonScriptLanguage.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,16 @@
5353
* An adapter of the Jython interpreter to the SciJava scripting interface.
5454
*
5555
* @author Johannes Schindelin
56+
* @author Mark Hiner <[email protected]>
5657
* @see ScriptEngine
5758
*/
5859
@Plugin(type = ScriptLanguage.class, name = "Python")
5960
public class JythonScriptLanguage extends AdaptedScriptLanguage {
6061

62+
// List of PhantomReferences and corresponding ReferenceQueue to
63+
// facilitate proper PhantomReference use.
64+
// See http://resources.ej-technologies.com/jprofiler/help/doc/index.html
65+
6166
private final LinkedList<JythonEnginePhantomReference> phantomReferences =
6267
new LinkedList<JythonEnginePhantomReference>();
6368
private final ReferenceQueue<JythonScriptEngine> queue =
@@ -76,10 +81,22 @@ public ScriptEngine getScriptEngine() {
7681
final JythonScriptEngine engine = new JythonScriptEngine();
7782

7883
synchronized (phantomReferences) {
84+
// NB: This phantom reference is used to clean up any local variables
85+
// created by evaluation of scripts via this ScriptEngine. We need
86+
// to use PhantomReferences because the "scope" of a script extends
87+
// beyond its eval method - a consumer may still want to inspect
88+
// the state of variables after evaluation.
89+
// By using PhantomReferences we are saying that the scope of
90+
// script evaluation equals the lifetime of the ScriptEngine.
7991
phantomReferences.add(new JythonEnginePhantomReference(engine, queue));
8092

81-
// If we added references to an empty queue, we need to start
82-
// a new polling thread
93+
// NB: The use of PhantomReferences requires a paired polling thread.
94+
// We poll instead of blocking for input to avoid leaving lingering
95+
// threads that would need to be interrupted - instead only starting
96+
// threads running when there is actually a PhantomReference that
97+
// will eventually be enqueued.
98+
// Here we check if there is already a polling thread in operation -
99+
// if not, we start a new thread.
83100
if (phantomReferences.size() == 1) {
84101
threadService.run(new Runnable() {
85102

@@ -93,6 +110,7 @@ public void run() {
93110
Thread.sleep(100);
94111

95112
synchronized (phantomReferences) {
113+
// poll the queue
96114
JythonEnginePhantomReference ref =
97115
(JythonEnginePhantomReference) queue.poll();
98116

@@ -103,7 +121,7 @@ public void run() {
103121
}
104122

105123
// Once we're done with our known phantom refs
106-
// we can shut down this thread.
124+
// we can let this thread shut down
107125
done = phantomReferences.size() == 0;
108126
}
109127

@@ -135,6 +153,10 @@ public Object decode(final Object object) {
135153
return object;
136154
}
137155

156+
/**
157+
* Helper class to clean up {@link PythonInterpreter} local variables when a
158+
* parent {@link JythonScriptEngine} leaves scope.
159+
*/
138160
private static class JythonEnginePhantomReference extends
139161
PhantomReference<JythonScriptEngine>
140162
{
@@ -153,14 +175,27 @@ public void cleanup() {
153175
PythonInterpreter interp = interpreter;
154176
if (interp == null) return;
155177

178+
// NB: This method for cleaning up local variables was taken from:
179+
// http://python.6.x6.nabble.com/Cleaning-up-PythonInterpreter-object-td1777184.html
180+
// Because Python is an interpreted language, when a Python script creates new
181+
// variables they stick around in a static org.python.core.PySystemState variable
182+
// (defaultSystemState) the org.python.core.Py class.
183+
// Thus an implicit static state is created by script evaluation, so we must manually
184+
// clean up local variables known to the interpreter when the scope of an executed
185+
// script is over.
186+
// See original bug report for the memory leak that prompted this solution:
187+
// http://fiji.sc/bugzilla/show_bug.cgi?id=1203
156188
final PyObject locals = interp.getLocals();
157189
for (final PyObject item : locals.__iter__().asIterable()) {
158190
final String localVar = item.toString();
191+
// Ignore __name__ and __doc__ variables, which are special and known not
192+
// to be local variables created by evaluation.
159193
if (!localVar.contains("__name__") && !localVar.contains("__doc__")) {
160-
194+
// Build list of variables to clean
161195
scriptLocals.add(item.toString());
162196
}
163197
}
198+
// Null out local variables
164199
for (final String string : scriptLocals) {
165200
interp.set(string, null);
166201
}

0 commit comments

Comments
 (0)