ICEfaces
  1. ICEfaces
  2. ICE-11220

WindowScope memory leak with JBoss EAP 6.4

    Details

    • Type: Bug Bug
    • Status: Closed
    • Priority: Major Major
    • Resolution: Fixed
    • Affects Version/s: EE-3.3.0.GA, EE-4.0.0.GA, EE-3.3.0.GA_P04
    • Fix Version/s: EE-4.2.0.GA, EE-3.3.0.GA_P05
    • Component/s: Framework
    • Labels:
      None
    • Environment:
      JBoss EAP 6.4, ICEfaces EE 3.3.0 P02, P03, P04

      Description

      A customer has found a potential memory leak seen in JBoss EAP 6. Here is the detailed analysis of the issue as well as a possible fix:

      We are switching from Glassfish 3.1 to JBoss EAP 6.4 and found a memory leak in
      IceFaces WindowScopeManager.

      === IceFaces WindowScope bean destruction ===

      IceFaces implements its own WindowScope managed by a WindowScopeManager which is a kind of sub-scope of the JSF ViewScope.

      When a WindowScope is destroyed, it removes the beans associated with the WindowScope from the ViewMap of the FacesContext, precisely those with @WindowDisposed in org.icefaces.impl.application.WindowScopeManager.disposeViewScopeBeans(FacesContext) (see attachment A).

      IceFaces does not destroyed the beans via a BeanManager which has created these beans, it just calls @PreDestroy is programmatically and removes them from the the ViewMap.

      ===JSF ViewScope bean destruction ===

      JSF Faces has a ViewScope which is implemented by class ViewScopeManager.

      When a ViewScope is destroyed, ViewScopeManager.destroyBeans(…) is called and all beans in its ViewMap are destroyed via com.sun.faces.mgbean.BeanManager.destroy(…) and com.sun.faces.mgbean.BeanBuilder).destroy(…). See Attachment B for details.

      === Memory leak in WindowScopeManager with JBoss EAP ===

      In Glassfish 3.1.x it does not seem to make a differences whether the beans are simply removed from the ViewMap or properly destroyed through the BeanManager.

      But JBoss 6.4 EAP manages an internal map with hard references to all managed beans, thus bypassing the BeanManager for destruction also bypasses org.jboss.as.web.deployment.WebInjectionContainer.destroyInstance(…)), and thus causes a memory leak. Each and every bean which is removed from the FacesContext ViewScope will kept referenced by JBoss.

      === Potential Fix in IceFaces WindowScopeManager ===

      We found a patch to fix this problem (see Attachment C). I was tested with a pretty large Selenium-based test suite and at least this major memory leak was fixed after applying this patch.

      Unfortunately both BeanManager and BeanBuilder are JSF Faces implementation classes which don’t belong to Faces API, same for any other involved class which technically could be used (see Attachment B). But on the other hand, IceFaces is already using com.sun.faces APIs anyway.

      We noticed that callAnnotatedMethod(…) is also called from other methods. The same patch might need to be applied for these to avoid further memory leaks.

      === Attchments ===

      Attachment A) original WindowScopeManager.disposeViewScopeBeans:

      (please pay attention to the 2 '// <-- HERE' marks)

      private static void disposeViewScopeBeans(FacesContext facesContext)
      {
      ExceptionHandler oldHandler = facesContext.getExceptionHandler();
      facesContext.setExceptionHandler(new DiscardingExceptionHandler(oldHandler));
      UIViewRoot viewRoot = facesContext.getViewRoot();
      if(null == viewRoot) {
      return;
      }
      Map viewMap = viewRoot.getViewMap();
      Iterator keys = viewMap.keySet().iterator(); {
      do {
      if(!keys.hasNext()) {
      break;
      }
      Object key = keys.next();
      Object object = viewMap.get(key);
      if(object.getClass().isAnnotationPresent(org.icefaces.bean.WindowDisposed.class)) {
      keys.remove(); // <-- HERE
      callAnnotatedMethod(object, javax/annotation/PreDestroy); // <-- HERE
      if(log.isLoggable(Level.FINE)) {
      log.log(Level.FINE, (new StringBuilder()).append("Closing window disposed ViewScoped bean ").append(key).toString());
      }
      }
      } while(true);
      facesContext.setExceptionHandler(oldHandler);
      }

      Attachment B) Complete stack for destroying a JSF ViewScope bean:

      org.jboss.as.web.deployment.ConcurrentReferenceHashMap<K,V>.remove(java.lang.Object) line: 1266
      org.jboss.as.web.deployment.WebInjectionContainer.destroyInstance(java.lang.Object) line: 71
      org.jboss.as.jsf.injection.JSFInjectionProvider.invokePreDestroy(java.lang.Object) line: 54
      com.sun.faces.mgbean.ManagedBeanBuilder(com.sun.faces.mgbean.BeanBuilder).destroy(com.sun.faces.spi.InjectionProvider, java.lang.Object) line: 116
      com.sun.faces.mgbean.BeanManager.destroy(java.lang.String, java.lang.Object) line: 282
      com.sun.faces.application.view.ViewScopeManager.destroyBeans(com.sun.faces.application.ApplicationAssociate, java.util.Map<java.lang.String,java.lang.Object>) line: 140
      com.sun.faces.application.view.ViewScopeManager.sessionDestroyed(javax.servlet.http.HttpSessionEvent) line: 297
      com.sun.faces.application.WebappLifecycleListener.sessionDestroyed(javax.servlet.http.HttpSessionEvent) line: 169
      com.sun.faces.config.ConfigureListener.sessionDestroyed(javax.servlet.http.HttpSessionEvent) line: 373
      org.apache.catalina.session.StandardSession.expire(boolean) line: 681
      org.apache.catalina.session.StandardSession.isValid() line: 576
      org.apache.catalina.session.StandardManager(org.apache.catalina.session.ManagerBase).processExpires() line: 387
      org.apache.catalina.session.StandardManager(org.apache.catalina.session.ManagerBase).backgroundProcess() line: 372
      org.apache.catalina.core.StandardContext(org.apache.catalina.core.ContainerBase).backgroundProcess() line: 1302
      org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(org.apache.catalina.Container, java.lang.ClassLoader) line: 1588
      org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(org.apache.catalina.Container, java.lang.ClassLoader) line: 1600
      org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(org.apache.catalina.Container, java.lang.ClassLoader) line: 1600
      org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run() line: 1574
      java.lang.Thread.run() line: 745

      Unfortuantely, none of these methods belongs to the JSF Faces API (see https://javaserverfaces.java.net/docs/2.1/javadocs/)

      Attachment C) patched WindowScopeManager.disposeViewScopeBeans:

      (please pay attention to the 3 '// <-- HERE' marks)

      private static void disposeViewScopeBeans(FacesContext facesContext)
      {
      ExceptionHandler oldHandler = facesContext.getExceptionHandler();
      facesContext.setExceptionHandler(new DiscardingExceptionHandler(oldHandler));
      UIViewRoot viewRoot = facesContext.getViewRoot();
      if(null == viewRoot) {
      return;
      }
      Map viewMap = viewRoot.getViewMap();
      Map<String, Object> windowMap = new HashMap<String, Object>(); // <-- HERE
      Iterator keys = viewMap.keySet().iterator(); {
      do {
      if(!keys.hasNext()) {
      break;
      }
      Object key = keys.next();
      Object object = viewMap.get(key);
      if(object.getClass().isAnnotationPresent(org.icefaces.bean.WindowDisposed.class)) {
      keys.remove();
      windowMap.put(key.toString(), object); // <-- HERE
      if(log.isLoggable(Level.FINE)) {
      log.log(Level.FINE, (new StringBuilder()).append("Closing window disposed ViewScoped bean ").append(key).toString());
      }
      }
      } while(true);

      // @PreDestroy methods will also be called by the bean manager
      com.sun.faces.application.view.ViewScopeManager.getInstance(facesContext).destroyBeans(facesContext, windowMap); // <-- HERE

      facesContext.setExceptionHandler(oldHandler);
      }

        Activity

        Hide
        Mircea Toma added a comment - - edited

        Applied solution that does not need to call JSF's non-public API.
        By calling ViewMap.clear method a PreDestroyViewMapEvent is fired and then captured by the ViewScopeManager who in turn invokes the com.sun.faces.spi.InjectionProvider implementation to dispose the view scope beans.

        Show
        Mircea Toma added a comment - - edited Applied solution that does not need to call JSF's non-public API. By calling ViewMap.clear method a PreDestroyViewMapEvent is fired and then captured by the ViewScopeManager who in turn invokes the com.sun.faces.spi.InjectionProvider implementation to dispose the view scope beans.
        Hide
        Mircea Toma added a comment - - edited

        The scopes test application should be used to find any potential regressions.

        Show
        Mircea Toma added a comment - - edited The scopes test application should be used to find any potential regressions.
        Hide
        Carmen Cristurean added a comment - - edited

        Verified ICEfaces4 trunk r50347 by running scopes regression tests on Tomcat7/Firefox49.

        Verified ICEfaces EE-3.3.0-maintenance branch r50347 by running scopes regression tests on Tomcat7 and JBoss EAP 6.4 (w/ default JSF v.2.1.28) in Firefox49, Chrome55, MsEdge38/ Windows10.

        Show
        Carmen Cristurean added a comment - - edited Verified ICEfaces4 trunk r50347 by running scopes regression tests on Tomcat7/Firefox49. Verified ICEfaces EE-3.3.0-maintenance branch r50347 by running scopes regression tests on Tomcat7 and JBoss EAP 6.4 (w/ default JSF v.2.1.28) in Firefox49, Chrome55, MsEdge38/ Windows10.

          People

          • Assignee:
            Mircea Toma
            Reporter:
            Arran Mccullough
          • Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved: