Testing a Java Memory Leak using System.gc() and WeakReference


When I worked on Liferay Faces, the team discovered a memory leak where JSF @RequestScoped, @ViewScoped, and @SessionScoped beans were never removed during Garbage Collection. The leak occurred in JBoss. JBoss expects Servlet event listeners to fire so it can clean up references to beans, but Liferay Faces runs in a Portlet environment, so JBoss never cleaned up its references. Fire the listener when the corresponding Portlet event occurs and voilà: memory leak fixed.

In order to prevent regressions, the team added a test that could verify that this issue did not recur. Unfortunately, the test required manual input. The tester would attach VisualVM to the app, generate a bunch of beans, log out via the browser, force garbage collection via VisualVM, and verify via VisualVM that all beans were cleaned up. I decided to automate this test. We already used Selenium for end-to-end testing so it was easy to automate the UI interactions that reproduced the leak. But I still had two roadblocks:

  1. How could I automate garbage collection (GC)?
  2. How could I track instances of leaked beans without creating new references that would ultimately prevent those beans from being removed?

Forcing GC turned out to be simple: I used System.gc(). It’s a little hacky since the docs state that calling System.gc() is only a best effort, but with OracleJDK and OpenJDK, it works reliably to force a complete GC (at least with the default collector). I added a button to the UI to call System.gc(), and the test worked. I added some logic to retry the GC a few times in case it failed to clean up the unreferenced beans with one pass, but I don’t believe the test has ever required more than a single GC.

I had a harder time figuring out how to track bean references without creating additional leaks until I stumbled upon WeakReference. WeakReferences keep a reference to an object until the object is marked as garbage and collected. Each bean constructor adds its own WeakReference to a list of references for all beans. I created a separate view to display the list. The test automation creates the beans, logs out of the application, and forces garbage collection. Then it checks the list of beans and if the list is empty or contains only null references, the test passes.1

Knowing what I know now, I would have probably written the test as an integration test and considered using the native JVM TI interface to force GC.2 Nevertheless, it was satisfying to replace a tedious manual test with automation—and learn about Java memory leaks, Java GC, WeakReferences, SoftReferences, and PhantomReferences in the process.


  1. You can find the code for the test app here:

  2. JVM TI ForceGarbageCollection has stronger guarantees than System.gc() about actually performing garbage collection, but using it would require native calls and binaries. That’s a lot of additional complexity over System.gc(). Since this is only a test, it’s probably not worth adding that complexity when System.gc() is reliable enough (especially with retries). 

Comments

Related Posts

Java Testing Tip #2:

Java Testing Tip #1:

Programming Terms Cheat Sheet

Technical Writing

Version Control