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

Posted

技术标签:

【中文标题】如何检测和删除(在会话期间)不能被垃圾收集的未使用的 @ViewScoped bean【英文标题】:How detect and remove (during a session) unused @ViewScoped beans that can't be garbage collected 【发布时间】:2015-08-05 07:07:53 【问题描述】:

编辑:codebulb.ch 在这篇文章中很好地解释和确认了这个问题提出的问题,包括 JSF @ViewScoped、CDI @ViewSCoped 和 Omnifaces @ViewScoped 之间的一些比较,以及明确的声明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 而不是附加到 GlassFish/Payara 的 JVisualVM 中强制 GC,我仍然在其中获得参考由com.sun.web.server.WebContainerListenercom.sun.web.server.WebContainerListener 类型的字段sessionListeners 持有(即使在调用@PreDestroy 之后),它们不会GC。


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

使用 h:link 获取新页面。

使用 h:outputLink(或 html A 标签)获取新页面。

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

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

相比之下,通过使用说 h:commandButton 来通过 JSF 导航系统会导致 @ViewScoped bean 的释放,以便它可以被垃圾收集。

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

[编辑:2016-11-13 现在还有一个改进的测试网络应用程序,其中包含完整的说明并与 OmniFaces @ViewScoped 和 GitHub 上的结果表进行比较:https://github.com/webelcomau/JSFviewScopedNav]

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

问:如何检测由 GET 导航引起的这种“悬挂/悬空”@ViewScoped bean 并将它们删除,或者以其他方式使它们可被垃圾回收?

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


【问题讨论】:

window.onbeforeunload。对于 OmniFaces 2.2 @ViewScoped,我有这个想法。 @BalusC 谢谢,我一定会给你的 OmniFaces2.2 ViewScoped 试一试(了解你目前处于 2.1-RC2)。 你是对的:没有理由调用处理程序:GET 请求不需要返回到服务器,因此不会触发任何服务器端组件。只有 ajax,就像 BalusC 暗示的那样,才能完成工作。我会尝试一些东西并提供一个样本 我在这里演示的简单测试项目当然只是为了调查一个大型 Web 应用程序中的这个问题,该应用程序大量使用 ViewScoped 并且目前(在某些情况下)内存问题令人望而却步。鉴于 JSF 社区对最近解决 ViewScoped bean 从未在会话结束时发布的问题有明确的兴趣(java.net/jira/browse/JAVASERVERFACES-2561,现在在最新的 Mojarra 中解决)我怀疑这里报告的这个问题也引起了广泛的兴趣,所以请坚持下去,欢迎提出任何建议。 @BalusC 新的测试网络应用程序将其他 JSF @ViewScoped bean 表单与此处的 OmniFaces 2.5.1 进行比较 github.com/webelcomau/JSFviewScopedNav,以及与结果表相关的 OmniFaces 特定问题:JSF: Mojarra vs. OmniFaces @ViewScoped: @PreDestroy called but bean can't be garbage collected 【参考方案1】:

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

在某些情况下,当调用浏览器unload 事件时,也可能需要立即销毁视图范围的bean。 IE。当用户通过 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 注释方法也将保证在浏览器卸载时被调用。

你可以在这里找到相关的源代码:

卸载脚本初始化器:ViewScopeManager#registerUnloadScript() 卸载脚本本身:unload.unminified.js 卸载视图处理程序:OmniViewHandler#unloadView() JSF 视图状态销毁器:Hacks#removeViewState()

卸载脚本将运行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 将失败:

    将物理视图数设置为 3(在 Mojarra 中,使用 com.sun.faces.numberOfLogicalViews 上下文参数,在 MyFaces 中使用 org.apache.myfaces.NUMBER_OF_VIEWS_IN_SESSION 上下文参数)。 创建一个引用标准 JSF @ViewScoped bean 的页面。 在选项卡中打开此页面并始终保持打开状态。 在另一个选项卡中打开同一页面,然后立即关闭此选项卡。 在另一个选项卡中打开同一页面,然后立即关闭此选项卡。 在另一个选项卡中打开同一页面,然后立即关闭此选项卡。 在第一个选项卡中提交表单。

这会因ViewExpiredException 而失败,因为在PreDestroyViewMapEvent 期间,先前关闭的选项卡的 JSF 视图状态并未物理销毁。他们仍然坚持在会议上。 OmniFaces @ViewScoped 实际上会摧毁它们。然而,销毁 JSF 视图状态是特定于实现的。这至少解释了 Hacks 类中应该实现这一点的相当 hacky 代码。

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


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

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

@Named
@ViewScoped
public class Bean implements Serializable 

我至少努力propose 一个公共API 方法来物理销毁JSF 视图状态。也许它会出现在 JSF 2.3 中,然后我应该能够消除 OmniFaces Hacks 类中的样板。一旦在 OmniFaces 中完善,它可能最终会出现在 JSF 中,但不会在 2.4 之前。

【讨论】:

感谢您的详细回复。我正在恢复对此的调查并使用最新的 OmniFaces 进行试验,至少在一个独立的测试应用程序中。通过将 'javax.faces.view.ViewScoped 替换为 org.omnifaces.cdi.ViewScoped' 来迁移我的生产应用程序是一个重大决定,因为它需要进行大量测试才能查看是否有任何其他副作用。据我所知,它没有进入 JSF2.3。问:您是否有任何消息说明这是否仍计划用于官方 JSF(如果有,何时可能可用)? [这并不是说我真的不会使用 OmniFaces 版本。] 我肯定会为 JSF.next 考虑它。我至少可以看出,我从事的几个生产应用程序都从中受益匪浅。在一个本地 JSF @ViewScoped 被大量使用的特定 web 应用上,内存使用量甚至减少了 80%。 感谢这个令人鼓舞的断言,我正在使用结果表 described here 的专用测试应用程序中恢复评估 org.omnifaces.cdi.ViewScoped(我注意到在大多数情况下都会调用 @PreDestroy 但由于某种原因引起了垃圾收集失败,可能是因为其他原因)。我还将在我的主 Web 应用程序的分支/分支中尝试它(在一些使用内存测量之后)。一旦我看到垃圾回收,我会接受你的回答。 巨大的进步(并完全接受答案)!我现在与我的测试应用程序在强制 GC 后得到完全一致的结果,在强制 GC 当我使用 NetBeans8.2 Profiler(内联)时总是只剩下 1 个基于omnifaces 的视图 bean(用于 1 个打开的浏览器选项卡) b> 而不是附加到 GlassFish/Payara 的 JVisualVM,在 ContainerBase$ContainerBackgroundProcessor 中,sessionListeners 类型为 com.sun.web.server.WebContainerListener 的字段仍然持有引用(即使在调用 @PreDestroy 之后),它们不会 GC。 确认 PreDestroy 总是被调用,并且可以使用 OmniFaces-2.6.6 @ViewScoped 对所有导航案例进行 GC,完整的结果序列和比较表在回答自己的问题 here。【参考方案2】:

好的,所以我拼凑了一些东西。

原理

现在不相关的 viewscoped bean 坐在那里,浪费每个人的时间和空间,因为在 GET 导航案例中,使用您突出显示的任何控件,都不会涉及服务器。如果不涉及服务器,则它无法知道 viewscoped bean 现在是多余的(即直到会话结束)。因此,这里需要一种方法来告诉服务器端您正在从中导航的视图需要终止其视图范围的 bean

约束

导航发生时应立即通知服务器端

    <h:body/> 中的beforeunloadunload 本来是理想的,但对于以下问题

    Browsers don't uniformly respect either of them

    使用其中任何一个的解决方案很可能需要一个超出 JSF 框架的 AJAX 解决方案。 JSF 的ajax-ready script 必须在表单的上下文中执行。您不能在表单中包含<h:body/>。我更喜欢将其全部保存在 JSF 中

    您不能在控件的onclick 中发送 ajax 请求,也不能在同一控件中导航。无论如何,都不是没有肮脏的弹出窗口。所以在h:buttonh:link 中导航onclick 是不可能的

肮脏的妥协

触发 ajax 请求 onclick,并让 PhaseListener 进行实际导航视图范围清理

食谱

    1 PhaseListenerViewHandler 也可以在这里使用;我选择前者,因为它更容易设置)

    JSF js API 的 1 个包装器

    中等程度的耻辱

让我们看看:

    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;
        
    
    
    

    JS 包装器

     function cleanViewScope()
      jsf.ajax.request(this, null, execute: 'someButton', target: this.href);
       return false;
      
    

    组合起来

      <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"/>
    

待办事项

    扩展h:link,可能添加一个属性来配置清除行为

    目标 url 的传递方式值得怀疑;可能会打开一个洞

【讨论】:

@kolussus 感谢您的回复(以及您在 *** 上的其他贡献)。提醒一下 ConfigurableNavigationHandler 可能性是件好事,但如图所示(有几个小的错字更正),它似乎不起作用。视图地图清除部分仅在 !isPostback 时调用,但在任何 GET 导航方法上,无论如何都不会调用 handleNavigation()。大量的 ViewScoped bean 仍然留在堆上,不能为任何 GET 导航方法进行垃圾收集。我现在已经在我的问题中包含了当前测试的完整代码。 您的 javascript 格式错误 'jsf.ajax.request(this, null, this,event, target:this.href);'。参考the jsDoc for jsf.ajax.request(source,event,options) 我尝试了 'jsf.ajax.request(this, null, target:this.href);'但是 this.href 是“未定义”,它给出了一个 JS 错误“类型错误未定义不是函数(评估 'context.element.hasAttribute("type"))'。感谢您的努力,但请先测试您的代码和答案,没有答案。 这是一个错字@WebelITAustralia;由我的手机上重新输入代码引起的;任何拥有基本文本编辑器的人都可以检测和修复的问题。它不会使答案背后的原则无效,而且我看不到其他人在尝试。您可以忽略答案/建议;这是你的特权。 我发现了问题(在你回复之前),当我使用这个内联 'onclick="jsf.ajax.request(this, null, target:this.href); return false; "' 它可以工作(使用 isAjax true 调用阶段侦听器),'this' 和 'this.href' 的上下文在放置在 JS 函数中时是错误的:cleanViewScope。您评论的这一部分完全没有必要“我没有看到其他人在尝试。您可以忽略答案/建议;那是您的特权”,并且是防御性的。即使您自愿无偿提供帮助,在 IDE 之外输入答案(您的所有代码都有错误)也会浪费您和我的时间。 我给你的答案是 1 分,但它实际上并没有完全解决我问的问题“如何检测和删除(在会话期间)未使用的不能被垃圾收集的 @ViewScoped bean ",因为它只捕获某些情况。尽管我是 JSF 的忠实拥护者,但这显然是该技术的一个重大缺陷,仅单击导航链接或按钮就会产生巨大的内存堆增长是荒谬的,并且是我的项目的一个主要问题。最终的解决方案可能涉及到 JSF 团队来解决它。欢迎对其他策略提出所有其他建议。

以上是关于如何检测和删除(在会话期间)不能被垃圾收集的未使用的 @ViewScoped bean的主要内容,如果未能解决你的问题,请参考以下文章

垃圾收集块级作用域

如果允许JVM在垃圾收集期间移动堆内存,那么垃圾收集如何不因移动指针而导致JNI爆炸?

java 垃圾收集

JVM垃圾回收机制

JVM垃圾回收机制

如何检测添加新的串口?