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:
- How could I automate garbage collection (GC)?
- 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
. WeakReference
s 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, WeakReference
s, SoftReference
s, and PhantomReference
s in the process.
-
You can find the code for the test app here:
-
JVM TI
ForceGarbageCollection
has stronger guarantees thanSystem.gc()
about actually performing garbage collection, but using it would require native calls and binaries. That’s a lot of additional complexity overSystem.gc()
. Since this is only a test, it’s probably not worth adding that complexity whenSystem.gc()
is reliable enough (especially with retries). ↩