Details
-
Type: Bug
-
Status: Closed
-
Priority: 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
-
Assignee Priority:P1
-
Support Case References:Support Case #14006 - https://icesoft.my.salesforce.com/5007000001hF9GD
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);
}
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);
}
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.