#268 Self reference in an Interpreter causes memory leak

open
nobody
General (151)
5
2006-07-27
2006-07-27
Stewart
No

I have included below the source for a very simple
test which can be run from the command line. Although
the test prints out memory usage, the phenomena is
best observed using a memory tool such a JProfiler. I
have also attached a screen-shot from JProfiler to
graphically illustrate the problem.

The core issue is this:
If I setup a self-referencing circle with the
Interpreter like so (in standard Java code):
{
this.interpreter = new bsh.Interpreter();
this.interpreter.set( "ref", this );
}
It sets up objects in the JVM memory which can never
ever be garbage collected, even though there is no
reference to them, and the object which is "this"
is out of scope in the calling method.

This is in contrast to, say a HashSet:
{
this.set = new HashSet();
onethis.set.add( this );
}
This setup does appear to garbage collect when
the "this" object goes out of scope.

The code for Test.java is below. Its output follows.
Note that after the self-referencing setup, roughly
90Kb has not been garbage collected, compared with
20Kb for the HashSet and 200 bytes for two simple java
objects.

Before tests
------------
10528
40
------------
Waiting ...

After object ref test
---------------------
26944
208
---------------------
Waiting ...

After HashSet test
------------------
158280
22760
------------------
Waiting ...

After Interpreter test
----------------------
362648
91352
----------------------
Waiting ...

If this test is run with JProfiler, it can be seen
that both the self-referencing Test objects, and the
self-referencing HashSet setup are successfully
garbage collected.

The screen shot from JProfiler shows the objects in
memory after the object of class Test has gone out of
scope - at the very final part of the program.

The Interpreter is not garbage collected.
Obviously, if you do this enough in an application, it
will cripple it for speed and eventually bomb out with
an "Out of heap space" error.

Any thoughts?

Regards,

Stewart

import java.io.*;
import java.util.*;
import bsh.*;

public class Test
{
private static Runtime r = Runtime.getRuntime();
private static long baseline = 0L;

public static void main(String[] args)
throws Exception
{
System.gc();
baseline = (r.maxMemory() - r.freeMemory());

report("Before tests");

twoObjectTest();

report("After object ref test");

hashSetTest();

report("After HashSet test");

interpreterTest();

report("After Interpreter test");
}

private Test ref = null;
private HashSet<Test> s = null;
private Interpreter i = null;

private static void twoObjectTest()
{
Test one = new Test();
Test two = new Test();

one.ref = two;
two.ref = one;
}

private static void hashSetTest()
{
Test one = new Test();

one.s = new HashSet<Test>();
one.s.add( one );
}

private static void interpreterTest()
throws EvalError
{
Test one = new Test();

one.i = new Interpreter();
one.i.set( "ref", one );
}

private static void report(String string)
throws IOException
{
char[] lines = new char[string.length()];
Arrays.fill(lines, '-');
System.out.println( );
System.out.println( string );
System.out.println( lines );
System.out.println( (r.maxMemory() - r.freeMemory
() - baseline) );
System.gc();
System.out.println( (r.maxMemory() - r.freeMemory
() - baseline) );
System.out.println( lines );
System.out.println("Waiting ...");
new BufferedReader( new InputStreamReader(
System.in )).readLine();
}

/**
* I thought this might solve the problem, but of
course, it never
gets called.
*/
protected void finalize()
throws Throwable
{
i.set( "ref", null );
i.unset( "ref" );
i = null;
}
}

Discussion

  • Stewart
    Stewart
    2006-07-27

    JProfiler screenshot

     
  • Logged In: NO

    Hi,

    My 'investigation' shows:

    A newly instantiated Interpreter can be found from the
    static field "Interpreter.sharedObject":
    Interpreter.
    sharedObject.
    namespace.
    classManager.
    declaringInterpreter
    (there exists 3 more paths that I found to the same
    interpreter object, no need to list them here)
    Corresponding class list is:
    java.lang.Class
    bsh.XThis
    bsh.NameSpace
    bsh.BshClassManager
    bsh.Interpreter

    A simple workaround is clear the static "sharedObject" field:
    (must use reflection since it is package scoped)
    Field[] fields = Interpreter.class.getDeclaredFields();
    for ( int i = 0; i < fields.length; i++ )
    {
    Field field = fields[i];
    if ( field.getName().equals( "sharedObject" ) )
    {
    System.out.println( "field = " + field );
    field.setAccessible( true );
    try
    {
    field.set( null, null );
    }
    catch ( IllegalAccessException e )
    {
    e.printStackTrace();
    }
    }
    }

    Should find a permanent fix for this problem. The expected
    behavior is that everything should be released when a
    Interpreter i non-reacheable. If one need some global
    sharing between interpreters, it should be set up 'manually'
    (like setting a boolean flag) so there is no mistake that
    some additional cleanup is needed in order to release all
    objects.

    Melv Ng