EDIT: The problem raised by this question is very well explained and confirmed in this article by codebulb.ch, including some comparison between JSF @ViewScoped
, CDI @ViewSCoped
, and the Omnifaces @ViewScoped
, and a clear statement that JSF @ViewScoped
is 'leaky by design': May 24, 2015 Java EE 7 Bean scopes compared part 2 of 2
EDIT: 2017-12-05 The test case used for this question is still extremely useful, however the conclusions concerning Garbage Collection in the original post (and images) were based on JVisualVM, and I have since found they are not valid. Use the NetBeans Profiler instead ! I am now getting completely consistent results for OmniFaces ViewScoped with the test app on forcing GC from within the NetBeans Profiler instead of JVisualVM attached to GlassFish/Payara, where I am getting references still held (even after @PreDestroy called) by field sessionListeners
of type com.sun.web.server.WebContainerListener
within ContainerBase$ContainerBackgroundProcessor
, and they won't GC.
It is known that in JSF2.2, for a page that uses a @ViewScoped bean, navigating away from it (or reloading it) using any of the following techniques will result in instances of the @ViewScoped bean "dangling" in the session so that it will not be garbage collected, leading to endlessly growing heap memory (as long as provoked by GETs):
Using an h:link to GET a new page.
Using an h:outputLink (or an HTML A tag) to GET a new page.
Reloading the page in the browser using a RELOAD command or button.
Reloading the page using a keyboard ENTER on the browser URL (also a GET).
By contrast, passing through the JSF navigation system by using say an h:commandButton results in the release of the @ViewScoped bean such that it can be garbage collected.
This is explained (by BalusC) at JSF 2.1 ViewScopedBean @PreDestroy method is not called and demonstrated for JSF2.2 and Mojarra 2.2.9 by my small NetBeans example project at https://stackoverflow.com/a/30410401/679457, which project illustrates the various navigation cases and is available for download here. (EDIT: 2015-05-28: The full code is now also available here below.)
[EDIT: 2016-11-13 There is now also an improved test web app with full instructions and comparison with OmniFaces @ViewScoped
and result table on GitHub here: https://github.com/webelcomau/JSFviewScopedNav]
I repeat here an image of the index.html, which summarises the navigation cases and the results for heap memory:
Q: How can I detect such "hanging/dangling" @ViewScoped beans caused by GET navigations and remove them, or otherwise render them garbage collectable ?
Please note that I am not asking how to clean them up when the session ends, I have already seen various solutions for that, I am looking for ways to clean them up during a session, so that heap memory does not grow excessively during a session due to inadvertent GET navigations.
Basically, you want the JSF view state and all view scoped beans to be destroyed during a window unload. The solution has been implemented in OmniFaces
@ViewScoped
annotation which is fleshed out in its documentation as below:You can find the relevant source code here:
ViewScopeManager#registerUnloadScript()
unload.unminified.js
OmniViewHandler#unloadView()
Hacks#removeViewState()
The unload script will run during window's
beforeunload
event, unless it's caused by any JSF based (ajax) form submit. As to commandlink and/or ajax submits, this is implementation specific. Currently Mojarra, MyFaces and PrimeFaces are recognized.The unload script will trigger
navigator.sendBeacon
on modern browsers and fall back to synchronous XHR (asynchronous would fail as page might be unloaded sooner than the request actually hits the server).The unload view handler will explicitly destroy all
@ViewScoped
beans, including standard JSF ones (do note that the unload script is only initialized when the view references at least one OmniFaces@ViewScoped
bean).This however doesn't destroy the physical JSF view state in the HTTP session and thus the below use case would fail:
com.sun.faces.numberOfLogicalViews
context param and in MyFaces useorg.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION
context param).@ViewScoped
bean.This would fail with a
ViewExpiredException
because the JSF view states of previously closed tabs aren't physically destroyed duringPreDestroyViewMapEvent
. They still stick around in the session. OmniFaces@ViewScoped
will actually destroy them. Destroying the JSF view state is however implementation specific. That explains at least the quite hacky code inHacks
class which should achieve that.The integration test for this specific case can be found in
ViewScopedIT#destroyViewState()
onViewScopedIT.xhtml
which is currently run against WildFly 10.0.0, TomEE 7.0.1 and Payara 4.1.1.163.In a nutshell: just replace
javax.faces.view.ViewScoped
byorg.omnifaces.cdi.ViewScoped
. The rest is transparent.I have at least made an effort to propose a public API method to physically destroy the JSF view state. Perhaps it will come in JSF 2.3 and then I should be able to eliminate the boilerplate in OmniFaces
Hacks
class. Once the thing is polished in OmniFaces, it will perhaps ultimately come in JSF, but not before 2.4.Okay, so I cobbled something together.
The Principle
The now-irrelevant viewscoped beans sit there, wasting everyone's time and space because in a GET navigation case, using any of the controls that you've highlighted, the server is not involved. If the server is not involved, it has no way of knowing the viewscoped beans are now redundant (that is until the session has died). So what's needed here is a way to tell the server-side that the view from which you're navigating, needs to terminate its view-scoped beans
The Constraints
The server-side should be notified as soon as the navigation is happening
beforeunload
orunload
in an<h:body/>
would have been ideal but for the following problemsBrowsers don't uniformly respect either of them
A solution using either of them will most likely require an AJAX solution that goes outside the JSF framework. JSF's ajax-ready script must be executed in the context of a form. You can't have
<h:body/>
inside a form. I prefer to keep it all inside JSFYou can't send an ajax request in
onclick
of a control, and also navigate in the same control. Not without a dirty popup anyway. So navigatingonclick
in ah:button
orh:link
is out of itThe dirty compromise
Trigger an ajax request
onclick
, and have aPhaseListener
do the actual navigation and the viewscope cleanupThe Recipe
1
PhaseListener
(aViewHandler
would also work here; I'm going with the former because it's easier to setup)1 wrapper around the JSF js API
A medium helping of shame
Let's see:
The
PhaseListener
The JS wrapper
Putting it together
To Do
Extend the
h:link
, possibly add an attribute to configure the clearing behaviourThe way the target url is being passed is suspect; might open up a hole