tags:

views:

133

answers:

3

I was intrigued by the answer to a similar question. I believe it is incorrect. So I created some test code. My question is, does this code prove/disprove/inconclusive the hypothesis that it is useful to nullify member variables in teardown methods? I tested it with JUnit4.8.1.

JUnit creates a new instance of the test class for each of the 4 tests. Each instance contains an Object obj. This obj is also inserted as the key of a static WeakHashMap. If and when JUnit releases its references to a test instance, the associated obj value will become weakly referenced and thus eligible for gc. The test tries to force a gc. The size of the WeakHashMap will tell me whether or not the objs are gc'ed. Some tests nullified the obj variable and others did not.

import org . junit . Before ;
import org . junit . After ;
import org . junit . Test ;
import java . util . ArrayList ;
import java . util . WeakHashMap ;
import java . util . concurrent . atomic . AtomicInteger ;
import static org . junit . Assert . * ;

public class Memory
{
    static AtomicInteger idx = new AtomicInteger ( 0 ) ;

    static WeakHashMap < Object , Object > map = new WeakHashMap < Object , Object > ( ) ;

    int id ;

    Object obj ;

    boolean nullify ;

    public Memory ( )
    {
    super ( ) ;
    }

    @ Before
    public void before ( )
    {
    id = idx . getAndIncrement ( ) ;
    obj = new Object ( ) ;
    map . put ( obj , new Object ( ) ) ;
    System . out . println ( "<BEFORE TEST " + id + ">" ) ;
    }

    void test ( boolean n )
    {
    nullify = n ;
    int before = map . size ( ) ;
    gc ( ) ;
    int after = map . size ( ) ;
    System . out . println ( "BEFORE=" + before + "\tAFTER=" + after ) ;
    }

    @ Test
    public void test0 ( )
    {
    test ( true ) ;
    }

    @ Test
    public void test1 ( )
    {
    test ( false ) ;
    }

    @ Test
    public void test2 ( )
    {
    test ( true ) ;
    }

    @ Test
    public void test3 ( )
    {
    test ( false ) ;
    }

    @ After
    public void after ( )
    {
    if ( nullify )
        {
        System . out . println ( "Nullifying obj" ) ;
        obj = null ;
        }
    System . out . println ( "<AFTER TEST " + id + ">" ) ;
    }

    /**
     * Try to force a gc when one is not really needed.
     **/
    void gc ( )
    {
    ArrayList < Object > waste = new ArrayList < Object > ( ) ;
    System . gc ( ) ; // only a suggestion but I'll try to force it
    list :
    while ( true ) // try to force a gc
        {
        try
            {
            waste . add ( new Object ( ) ) ;
            }
        catch ( OutOfMemoryError cause )
            {
            // gc forced? should have been
            waste = null ;
            break list ;
            }
        }
    System . gc ( ) ; // only a suggestion but I tried to force it
    }
}

I ran the code using the command line interface (utilizing the -Xmx128k option to increase garbage collection) and got the following result

.<BEFORE TEST 0>
BEFORE=1    AFTER=1
Nullifying obj
<AFTER TEST 0>
.<BEFORE TEST 1>
BEFORE=2    AFTER=1
<AFTER TEST 1>
.<BEFORE TEST 2>
BEFORE=2    AFTER=1
Nullifying obj
<AFTER TEST 2>
.<BEFORE TEST 3>
BEFORE=2    AFTER=1
<AFTER TEST 3>

The Test0 obj was nullified and in Test1 it is gc'ed. But the Test1 obj was not nullified and it got gc'ed in Test2. This suggests that nullifying objects is not necessary.

A: 

It is indeed not necessary, but it does help the garbage collector when it needs to know what variables are used or not; null variable are pretty much garantied to be good candidate for garbage collection.

Drahakar
But in my experiment, the gc collector always managed to figure it out whether or not I nullified.
emory
Have you ever worked with different garbage collectors? Some might be pretty good, but there are some garbage collectors that need a little bit of help
Drahakar
'It does help the garbage collector'. No it doesn't. The entire object is released after each test. Nulling the object references doesn't help in the slighest.
EJP
From he official jUnit page : "When are tests garbage collected? [...]Therefore, if you allocate external or limited resources in a test, you are responsible for freeing those resources. Explicitly setting an object to null in the tearDown() method, for example, allows it to be garbage collected before the end of the entire test run." (http://junit.sourceforge.net/doc/faq/faq.htm)
Drahakar
@Drahakar if my gc is superior to others, then that would mean I could write code and tests that compile and pass on my computer but compile and fail (due to memory leak) on other ppls computers. My JUnit tests are platform dependent?
emory
If I understand the official JUnit page correctly, nullification is necessary (for garbage collection). I emailed the FAQ maintainer for clarification.
emory
+2  A: 

No it is not necessary.

Tear-down methods are for life-cycled objects that likes to be explicitly closed, terminated, shutdown, disposed, disconnected, unregistered or whatever.

Even if your references survive to the next test case, they will be overwritten by your set-up method and become unreferenced and thus eligible for garbage collection.

And event if JUnit creates a new instance of your test case for every method (which seems to be the case), those test objects are not held on to. At least not if the test passes, according to a quick experiment. So the lot of it will be collected at leisure anyway.

Christian Vest Hansen
Both JUnit3 style tests and JUnit4 style tests create a separate object per test method that is executed, so unless the references are stored in statics (not recommended) the references will not be overwritten by the next set-up method
NamshubWriter
@NamshubWriter is that a guarantee or an implementation detail? If the latter then it might change (though unlikely). Regardless, these instances aren't kept around (in JUnit 4.8.1, according to experiment) — at least not if the test passes.
Christian Vest Hansen
@NamshubWriter I updated my answer. Thanks for info.
Christian Vest Hansen
It's not an implementation detail, it's the documented behavior. JUnit does this so it's harder to have the result of one test affect the behavior of another test.
NamshubWriter
+3  A: 

JUnit 4.x style tests and test suites handle this differently than JUnit 3.x test suites.

With JUnit 3.x style tests, a TestSuite contains references to other Test objects (which may be TestCase objects or other TestSuite objects). If you create a suite with many tests, then there will be hard references to all of the leaf TestCase objects for the entire run of the outermost suite. If some of your TestCase objects allocate objects in setUp() that take up a lot of memory, and references to those objects are stored in fields that are not set to null in tearDown(), then you might have a memory problem.

In other words, for JUnit 3.x style tests, the specification of which tests to run references the actual TestCase objects. Any objects reachable from a TestCase object will be kept in memory during the test run.

For JUnit 4.x style tests, the specification of which tests to run uses Description objects. The Description object is a value object that specifies what to run, but not how to run it. The tests are run by a Runner object that takes the Description of the test or suite and determines how to execute the test. Even the notification of the status of the test to the test listener uses the Description objects.

The default runner for JUnit4 test cases, JUnit4, keeps a reference to the test object around only for the duration of the run of that test. If you use a custom runner (via the @RunWith annotation), that runner may or may not keep references to the tests around for longer periods of time.

Perhaps you are wondering what happens if you include a JUnit3-style test class in a JUnit4-style Suite? JUnit4 will call new TestSuite(Class) which will create a separate TestCase instance per test method. The runner will keep a reference to the TestSuite for the entire life of the test run.

In short, if you are writing JUnit4-style tests, do not worry about setting your test case's fields to null in a tear down (do, of course, free resources). If you are writing JUnit3-style tests that allocate large objects in setUp() and store those objects in fields of the TestCase, consider setting the fields to null.

NamshubWriter
This is the best answer. Nullifying is necessary (to avoid memory leakage) in JUnit 3.x. If you use JUnit4.x and the default runner nullifying is not necessary. If you use a custom runner, then you may need to nullify.
emory