如何检测和删除(在会话期间)未被垃圾回收的未使用的@ViewScoped bean

时间:2015-05-23 08:30:40

标签: jsf jsf-2 garbage-collection heap-memory view-scope

编辑:这个问题提出的问题在本文中由codebulb.ch进行了很好的解释和确认,包括JSF @ViewScoped,CDI @ViewSCoped和Omnifaces {{1}之间的一些比较。并且明确声明JSF @ViewScoped是'设计泄漏':May 24, 2015 Java EE 7 Bean scopes compared part 2 of 2

编辑:2017-12-05用于此问题的测试用例仍然非常有用,但是原始帖子(和图像)中关于垃圾收集的结论是基于JVisualVM,我发现它们无效。 改为使用NetBeans Profiler!我现在获得OmniFaces ViewScoped与NetBeans Profiler中强制GC的测试应用程序的完全一致的结果,而不是连接到GlassFish / Payara的JVisualVM,我仍然在那里获得参考在@ViewScoped内按类型sessionListeners的字段com.sun.web.server.WebContainerListener保留(甚至在@PreDestroy调用之后),并且它们不会是GC。

众所周知,在JSF2.2中,对于使用@ViewScoped bean的页面,使用以下任何一种技术导航(或重新加载)将导致@ViewScoped bean“悬空”的实例会话,以便它不会被垃圾收集,导致堆内存不断增长(只要GET引发):

  • 使用h:链接获取新页面。

  • 使用h:outputLink(或HTML A标记)获取新页面。

  • 使用RELOAD命令或按钮在浏览器中重新加载页面。

  • 使用键盘在浏览器URL(也是GET)上重新加载页面。

相比之下,使用say a h:commandButton通过JSF导航系统会导致@ViewScoped bean的发布,从而可以对其进行垃圾回收。

这是由(BalusC)在JSF 2.1 ViewScopedBean @PreDestroy method is not called解释的,并在我https://stackoverflow.com/a/30410401/679457的小型NetBeans示例项目中针对JSF2.2和Mojarra 2.2.9进行了演示,该项目说明了各种导航案例,并且{ {3}}。 (编辑:2015-05-28:现在也可以在下面找到完整的代码。)

[编辑:2016-11-13现在还有一个改进的测试网络应用程序,其中包含完整说明并与OmniFaces ContainerBase$ContainerBackgroundProcessor和GitHub上的结果表进行比较:available for download here

我在这里重复一个index.html的图片,它总结了导航案例和堆内存的结果:

enter image description here

问:如何检测由GET导航导致的“悬挂/悬挂”@ViewScoped bean并将其删除,或以其他方式呈现垃圾可收集?

请注意,我不会在会话结束时询问如何清理它们,我已经看到了各种解决方案,我正在寻找在会话期间清理它们的方法,以便堆内存确实如此由于无意的GET导航,会话期间不会过度增长。

2 个答案:

答案 0 :(得分:5)

基本上,您希望在窗口卸载期间销毁JSF视图状态和所有视图范围的bean。该解决方案已在OmniFaces @ViewScoped annotation中实施,其文档如下所述:

  

在调用浏览器unload事件时,可能会出现立即销毁视图范围bean的情况。即当用户通过GET导航或关闭浏览器选项卡/窗口时。两个JSF 2.2视图范围注释都不支持这一点。从OmniFaces 2.2开始,此CDI视图范围注释将保证在浏览器卸载时也调用@PreDestroy带注释的方法。这个技巧是通过自动包含的帮助脚本omnifaces:unload.js通过同步XHR请求完成的。然而,有一个小警告:在慢速网络和/或糟糕的服务器硬件上,卸载页面的最终用户操作与所需结果之间可能存在明显的滞后。如果这是不可取的,那么最好坚持使用JSF 2.2自己的视图范围注释并接受推迟的销毁。

     

自OmniFaces 2.3以来,卸载进一步改进,以便在服务器端状态保存的情况下从JSF实现的内部LRU映射中物理移除关联的JSF视图状态,从而进一步降低ViewExpiredException在另一个上的风险先前创建/打开的视图。作为此更改的副作用,在OmniFaces CDI视图范围bean的同一视图中引用的任何标准JSF视图范围bean的@PreDestroy注释方法也将保证在浏览器卸载时调用。

您可以在此处找到相关的源代码:

卸载脚本将运行during窗口的beforeunload事件,除非由任何基于JSF的(ajax)表单提交引起。至于commandlink和/或ajax提交,这是特定于实现的。 Currently Mojarra,MyFaces和PrimeFaces被认可。

现代浏览器上的卸载脚本将trigger navigator.sendBeacon并回退到同步XHR(异步会失败,因为页面可能比实际命中服务器的请求更快卸载)。

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);
}

卸载视图处理程序将explicitly销毁所有@ViewScoped bean,包括标准的JSF bean(请注意,只有在视图引用至少一个OmniFaces @ViewScoped bean时才会初始化卸载脚本)。

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

然而,这不会破坏HTTP会话中的物理JSF视图状态,因此下面的use case会失败:

  1. 将物理视图数设置为3(在Mojarra中,使用com.sun.faces.numberOfLogicalViews上下文参数,在MyFaces中使用org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION上下文参数。
  2. 创建一个引用标准JSF @ViewScoped bean的页面。
  3. 在标签页中打开此页并始终保持打开状态。
  4. 在另一个标签页中打开同一页面,然后立即关闭此标签页。
  5. 在另一个标签页中打开同一页面,然后立即关闭此标签页。
  6. 在另一个标签页中打开同一页面,然后立即关闭此标签页。
  7. 在第一个标签中提交表单。
  8. 这会因ViewExpiredException而失败,因为在PreDestroyViewMapEvent期间,以前关闭的标签的JSF视图状态不会被物理销毁。他们仍然坚持参加会议。 OmniFaces @ViewScoped实际上会摧毁它们。但是,销毁JSF视图状态是特定于实现的。这至少解释了Hacks类中应该实现的非常hacky代码。

    针对此特定案例的集成测试可在ViewScopedIT#destroyViewState() ViewScopedIT.xhtml currently上找到,propose针对WildFly 10.0.0,TomEE 7.0.1和Payara 4.1.1.163运行。< / p>

    简而言之:只需将javax.faces.view.ViewScoped替换为org.omnifaces.cdi.ViewScoped。其余的都是透明的。

    import javax.inject.Named;
    import org.omnifaces.cdi.ViewScoped;
    
    @Named
    @ViewScoped
    public class Bean implements Serializable {}
    

    我至少努力{{3}}一个公共API方法来物理破坏JSF视图状态。也许它会出现在JSF 2.3中,然后我应该能够消除OmniFaces Hacks类中的样板。一旦OmniFaces中的东西被抛光,它可能最终会出现在JSF中,但不会出现在2.4之前。

答案 1 :(得分:2)

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