How detect and remove (during a session) unused @ViewScoped beans that can't be garbage collected
Asked Answered
T

2

14

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://mcmap.net/q/17960/-jsf-2-1-viewscopedbean-predestroy-method-is-not-called, 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:

enter image description here

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.


Tribromoethanol answered 23/5, 2015 at 8:30 Comment(5)
window.onbeforeunload. I have this in mind for OmniFaces 2.2 @ViewScoped.Neutralize
@Neutralize Thanks, I will definitely give your OmniFaces2.2 ViewScoped a go (understand you are currently at 2.1-RC2).Tribromoethanol
You're right: there's no reason for the handler to be called: GET requests need not come back to the server and as a result, no server-side components will be triggered. Only ajax, like BalusC has hinted, can get the job done. I'll try something out and give a sampleLocarno
The simple test project I have demonstrated here is of course merely to investigate this problem in a large web application that makes heavy use of ViewScoped and is currently suffering prohibitively from memory problems (under certain circumstances). Given the clear JSF community interest in the recent resolution of the problem with ViewScoped beans never being released at the end of a session (java.net/jira/browse/JAVASERVERFACES-2561, now resolved in latest Mojarra) I suspect this problem reported here is also of wide interest, so please do persist, any suggestions are most welcome.Tribromoethanol
@Neutralize New test web app comparing other JSF @ViewScoped bean forms with OmniFaces 2.5.1 here github.com/webelcomau/JSFviewScopedNav, and related OmniFaces-specific question with results tables: JSF: Mojarra vs. OmniFaces @ViewScoped: @PreDestroy called but bean can't be garbage collectedTribromoethanol
N
10

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:

There may be cases when it's desirable to immediately destroy a view scoped bean as well when the browser unload event is invoked. I.e. when the user navigates away by GET, or closes the browser tab/window. None of the both JSF 2.2 view scope annotations support this. Since OmniFaces 2.2, this CDI view scope annotation will guarantee that the @PreDestroy annotated method is also invoked on browser unload. This trick is done by a synchronous XHR request via an automatically included helper script omnifaces:unload.js. There's however a small caveat: on slow network and/or poor server hardware, there may be a noticeable lag between the enduser action of unloading the page and the desired result. If this is undesireable, then better stick to JSF 2.2's own view scope annotations and accept the postponed destroy.

Since OmniFaces 2.3, the unload has been further improved to also physically remove the associated JSF view state from JSF implementation's internal LRU map in case of server side state saving, hereby further decreasing the risk at ViewExpiredException on the other views which were created/opened earlier. As side effect of this change, the @PreDestroy annotated method of any standard JSF view scoped beans referenced in the same view as the OmniFaces CDI view scoped bean will also guaranteed be invoked on browser unload.

You can find the relevant source code here:

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).

var url = form.action;
var query = "omnifaces.event=unload&id=" + id + "&" + VIEW_STATE_PARAM + "=" + encodeURIComponent(form[VIEW_STATE_PARAM].value);
var contentType = "application/x-www-form-urlencoded";

if (navigator.sendBeacon) {
    // Synchronous XHR is deprecated during unload event, modern browsers offer Beacon API for this which will basically fire-and-forget the request.
    navigator.sendBeacon(url, new Blob([query], {type: contentType}));
}
else {
    var xhr = new XMLHttpRequest();
    xhr.open("POST", url, false);
    xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
    xhr.setRequestHeader("Content-Type", contentType);
    xhr.send(query);
}

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).

context.getApplication().publishEvent(context, PreDestroyViewMapEvent.class, UIViewRoot.class, createdView);

This however doesn't destroy the physical JSF view state in the HTTP session and thus the below use case would fail:

  1. Set number of physical views to 3 (in Mojarra, use com.sun.faces.numberOfLogicalViews context param and in MyFaces use org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION context param).
  2. Create a page which references a standard JSF @ViewScoped bean.
  3. Open this page in a tab and keep it open all time.
  4. Open the same page in another tab and then immediately close this tab.
  5. Open the same page in another tab and then immediately close this tab.
  6. Open the same page in another tab and then immediately close this tab.
  7. Submit a form in the first tab.

This would fail with a ViewExpiredException because the JSF view states of previously closed tabs aren't physically destroyed during PreDestroyViewMapEvent. 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 in Hacks class which should achieve that.

The integration test for this specific case can be found in ViewScopedIT#destroyViewState() on ViewScopedIT.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 by org.omnifaces.cdi.ViewScoped. The rest is transparent.

import javax.inject.Named;
import org.omnifaces.cdi.ViewScoped;

@Named
@ViewScoped
public class Bean implements Serializable {}

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.

Neutralize answered 11/11, 2016 at 16:23 Comment(7)
Thanks for your detailed reply. I am resuming investigation of this and trials with latest OmniFaces, at least in an isolated test app. Migrating my production app by replacing 'javax.faces.view.ViewScoped by org.omnifaces.cdi.ViewScoped' throughout is a big decision, as it would require a lot of testing to see whether there are any other side-effects. As far as I can tell it did not make it into JSF2.3. Q: Do you have any news on whether this is still planned for official JSF (and if so when it is likely to be available) ? [Which is not to say I won't indeed use the OmniFaces version.]Tribromoethanol
I'll surely consider it for JSF.next. I can at least tell that several production apps I've worked on have hugely benefited from this. On one particular webapp where the native JSF @ViewScoped was heavily used the memory usage has even decreased for 80%.Neutralize
Thanks for this encouraging assertion, I'm resuming assessing org.omnifaces.cdi.ViewScoped in a dedicated test app with results table described here (where I noted that @PreDestroy is invoked under most cases but for some reason provoked garbage collection is failing, maybe because of something else). I'll also try it in a branch/fork of my main web app (after some usage memory measurements). I'll accept your answer once I can see garbage collection.Tribromoethanol
Massive progress (and answer fully accepted) ! I am now getting completely consistent results with my test app on forcing GC with always just 1 omnifaces-based view bean left (for 1 open browser tab) after forced GC when I use the NetBeans8.2 Profiler (inline) 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.Tribromoethanol
Confirming PreDestroy always called and can GC for all navigation cases with OmniFaces-2.6.6 @ViewScoped, full sequence of results with comparison tables at answer to own question here.Tribromoethanol
@Balus Thanks! We are using JSF Viewscoped in our application, the objects we store in view scoped beans are big in size and number, because of that we are getting Out of memory error in Automation env. We realised that's design issue. After reading many posts, I have changed code by replacing JSF Viewscoped to Ominifaces Viewscoped. Now there's no Out of memory error. That's good. I understand when user navigation happens with get request the preDestroy will be called and unreference the object. What happens to the beans when the user doesn't logout or navigate away but closes the browser?Hairspring
@ddc: it'll also be destroyed as long as the browser close wasn't caused by browser software crash or a hardware crash (i.e. the browser had the full opportunity to fire the unload event). Add a @PreDestroy method with a breakpoint or logger to confirm. If the close was caused by a crash then beans will live until session expires server-side.Neutralize
L
3

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

  1. beforeunload or unload in an <h:body/> would have been ideal but for the following problems

  2. You 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 navigating onclick in a h:button or h:link is out of it

The dirty compromise

Trigger an ajax request onclick, and have a PhaseListener do the actual navigation and the viewscope cleanup

The Recipe

  1. 1 PhaseListener (a ViewHandler would also work here; I'm going with the former because it's easier to setup)

  2. 1 wrapper around the JSF js API

  3. A medium helping of shame

Let's see:

  1. The PhaseListener

    public ViewScopedCleaner implements PhaseListener{
    
        public void afterPhase(PhaseEvent evt){
             FacesContext ctxt = event.getFacesContext();
             NavigationHandler navHandler = ctxt.getApplication().getNavigationHanler();
             boolean isAjax =  ctx.getPartialViewContext().isAjaxRequest(); //determine that it's an ajax request
             Object target = ctxt.getExternalContext().getRequestParameterMap().get("target"); //get the destination URL
    
                    if(target !=null && !target.toString().equals("")&&isAjax ){
                         ctxt.getViewRoot().getViewMap().clear(); //clear the map
                         navHandler.handleNavigation(ctxt,null,target);//navigate
                     }
    
        }
    
        public PhaseId getPhaseId(){
            return PhaseId.APPLY_REQUEST_VALUES;
        }
    
    }
    
  2. The JS wrapper

     function cleanViewScope(){
      jsf.ajax.request(this, null, {execute: 'someButton', target: this.href});
       return false;
      }
    
  3. Putting it together

      <script>
         function cleanViewScope(){
             jsf.ajax.request(this, null, {execute: 'someButton', target: this.href}); return false;
          }
      </script>  
    
     <f:phaseListener type="com.you.test.ViewScopedCleaner" />
     <h:link onclick="cleanViewScope();" value="h:link: GET: done" outcome="done?faces-redirect=true"/>
    

To Do

  1. Extend the h:link, possibly add an attribute to configure the clearing behaviour

  2. The way the target url is being passed is suspect; might open up a hole

Locarno answered 24/5, 2015 at 21:40 Comment(11)
@kolussus Thanks for your reply (and for your other contributions here on stackoverflow). It is good to be reminded of the ConfigurableNavigationHandler possibility, but as shown (with a couple of minor typo corrections) it does not seem to work. The view map clearing part is only called when !isPostback, but on any of the GET navigation methods the handleNavigation() is never invoked anyway. Copious ViewScoped beans are still left on the heap and cant be garbage collected for any of the GET navigation methods. I have included now the complete code of the current test in my question.Tribromoethanol
Your javascript is ill-formed 'jsf.ajax.request(this, null, {this,event, {target:this.href});'. With reference to the jsDoc for jsf.ajax.request(source,event,options) I tried 'jsf.ajax.request(this, null, {target:this.href});' but this.href is 'undefined' and it gives a JS error 'typeerror undefined is not a function (evaluating 'context.element.hasAttribute("type"))'. Thanks for your efforts but please do test your code and answers first, neither answer worked yet.Tribromoethanol
It's a typo @WebelITAustralia; caused by retyping code on my phone; one that anyone with a basic text editor would be able to detect and fix. It doesn't invalidate the principle behind the answer and I don't see anyone else trying. You can ignore the answer/suggestion; that's your prerogative.Locarno
I figured out the problem (before you replied), when I use this inline 'onclick="jsf.ajax.request(this, null, {target:this.href}); return false;"' it works (calls the phase listener with isAjax true), the context for 'this' and 'this.href' is wrong when placed in the JS function: cleanViewScope. This part of your comment is completely unnecessary "I don't see anyone else trying. You can ignore the answer/suggestion; that's your prerogative", and defensive. Even when you volunteer help unpaid, typing answers outside an IDE (all of your code had errors) wastes your time and mine.Tribromoethanol
I am giving your answer a 1 up, but it does not in fact address completely the question I asked "How detect and remove (during a session) unused @ViewScoped beans that can't be garbage collected", because it is only catching some cases. Although I am a huge fan of JSF, this is clearly a major flaw with the technology, having massive memory heap growth as a side-effect of just clicking on navigaiton links or buttons is ridiculous, and a major headache for my project. The ultimate solution perhaps involves the JSF team addressing it. All other suggestions for other strategies are most welcome.Tribromoethanol
If somebody else comes along and see that I have marked your current answer as +1 (correct) and then blindly copies it into their IDE they will find: 1. That there are some simple variable typos (the IDE will alert them to it), easily fixed; 2. That it does not work, because the JavaScript function is not correctly written (even if somebody "should" be able to read between the lines and fix it). Therefore please edit your answer and correct the JavaScript function so that it works (I will also test it again at my end), to fairly earn the point this time, even with your reputation.Tribromoethanol
You seem to have edited it (thanks for that) but it still does not work with this now in the JS function 'jsf.ajax.request(this, null, {execute: 'someButton', target: this.href});'. For the answer to be correct (run properly), it needs to show what 'showButton' refers to and how. As written, it does not invoke the PhaseListener from the h:link as it is shown.Tribromoethanol
I understand @WebelITAustralia. Please retract your upvote. What's here works for me on a JSF2.2/glassfish-4/Chrome setup. If you want to discuss the specifics of what you're seeing in your JS/browser console, that can happen in chat.Locarno
'Please retract your upvote.' Why ? The upvote is for 'This answer is useful', not for a 100% correct answer (that runs). You write that 'What's here works for me' but in the JS script as given in your answer it refers to 'execute: someButton' while under the h:link you have no reference to someButton, you have 'h:link onclick="cleanViewScope();" value="h:link: GET: done" outcome="done?faces-redirect=true"'. Again, I can't give you a vote for a correct (as opposed to useful) answer unless the code as given in the answer works. Please check what you have in your own test against it.Tribromoethanol
And the JavaScript you have in your current stackoverflow answer has 'function cleanViewScope(){ jsf.ajax.request(this, null, {execute: 'someButton', target: this.href}); return false; }', where 'this.href' is 'undefined', likely because the context is wrong, the 'this' is by the time it is invoked is '[object Window]'. You wrote: "What's here works for me on a JSF2.2/glassfish-4/Chrome setup. ". That is the same setup I have, and I can't see how your current answer as written can work with "<h:link onclick="cleanViewScope();" value="h:link: GET: done" outcome="done?faces-redirect=true"'>".Tribromoethanol
The problem raised by this question is very well explained and confirmed in this article by codebulb.ch, including some comparison with 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 2Tribromoethanol

© 2022 - 2024 — McMap. All rights reserved.