Bug 58486

Summary: JreMemoryLeakPreventionListener: initialize two further JRE classes
Product: Tomcat 8 Reporter: Luke Woodward <luke.woodward>
Component: CatalinaAssignee: Tomcat Developers Mailing List <dev>
Status: RESOLVED FIXED    
Severity: minor    
Priority: P2    
Version: 8.0.x-trunk   
Target Milestone: ----   
Hardware: PC   
OS: All   

Description Luke Woodward 2015-10-07 22:11:12 UTC
I would like to propose adding a further couple of classes to those that Tomcat's JreMemoryLeakPreventionListener statically initializes.

The classes com.sun.org.apache.xerces.internal.dom.DOMNormalizer and com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl, both within rt.jar, each contain a static final field of type RuntimeException named 'abort'.  When these classes are statically initialized, these exceptions are created and their stacktraces filled in.  If a web app class happens to be in the call stack when either class's exception is created, this class cannot then be garbage collected when the web app is stopped because an exception in a static field of a class has a reference to it.  This then causes a PermGen leak as the web apps's classloader, and all of the classes it loaded, cannot be garbage-collected.

To reproduce this issue, use the following servlet class:

package com.example;

import javax.servlet.ServletException;
import javax.servlet.http.*;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.ls.DOMImplementationLS;

public class DOMNormalizerLeakServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException {
        try {
            Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
            document.createElement("test");
            DOMImplementationLS implementation = (DOMImplementationLS)document.getImplementation();
            implementation.createLSSerializer().writeToString(document);
            response.getWriter().write("done");
        }
        catch (Exception e) {
            throw new ServletException(e);
        }
    }
}

and the following web.xml file:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
	 version="3.1">
    <servlet>
        <servlet-name>test</servlet-name>
        <servlet-class>com.example.DOMNormalizerLeakServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>test</servlet-name>
        <url-pattern>/index.html</url-pattern>
    </servlet-mapping>   
</web-app>

I've reproduced this problem with the latest Tomcat (8.0.27) and the latest Oracle JDK8 (1.8.0_60), by:

* deploying a web app consisting of the above servlet class and deployment descriptor to Tomcat,
* viewing the index.html page generated by Tomcat (the browser should show the word 'done'),
* reloading the web app using the Tomcat manager app,
* clicking the 'Find leaks' button in the Diagnostics section of the manager app, which reveals a possible memory leak,
* using a profiler such as JVisualVM to confirm that Tomcat now has a 'destroyed' classloader that could not be garbage collected because there is a chain of references from a JRE class to the servlet class it loaded.

Here's a path from the classloader to the exception, which I obtained with the help of JVisualVM:

this     - value: org.apache.catalina.loader.WebappClassLoader #3
 <- <classLoader>     - class: com.example.DOMNormalizerLeakServlet, value: org.apache.catalina.loader.WebappClassLoader #3
  <- [2]     - class: java.lang.Object[], value: com.example.DOMNormalizerLeakServlet class DOMNormalizerLeakServlet
   <- [2]     - class: java.lang.Object[], value: java.lang.Object[] #4319
    <- backtrace     - class: java.lang.RuntimeException, value: java.lang.Object[] #4318
     <- abort (sticky class)     - class: com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl, value: java.lang.RuntimeException #1

There is a straightforward workaround for this: add the names of these two classes to the classesToInitialize attribute for the JreMemoryLeakPreventionListener.  This then causes the classes to be statically initialized by Tomcat itself and keeps web app classes out of the stacktrace of these exceptions.

I have filed a bug report with Oracle to change the behaviour of these two classes.  However, until this gets fixed (if it gets fixed at all), it would be appreciated if the JreMemoryLeakPreventionListener could be adapted to handle these two classes.
Comment 1 Luke Woodward 2015-10-09 21:37:01 UTC
The servlet in my previous comment demonstrates a leak with the 'abort' exception in the class com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl.  If you replace the lines

            document.createElement("test");
            DOMImplementationLS implementation = (DOMImplementationLS)document.getImplementation();
            implementation.createLSSerializer().writeToString(document);

with

            document.normalizeDocument();

then this causes a leak with com.sun.org.apache.xerces.internal.dom.DOMNormalizer.abort instead.
Comment 2 Mark Thomas 2015-10-25 13:39:13 UTC
Fixed in trunk, 8.0.x (for 8.0.29) and 7.0.x (for 7.0.66).
Comment 3 Konstantin Kolinko 2015-10-25 23:08:19 UTC
I raised this issue with Apache Xerces,
https://issues.apache.org/jira/browse/XERCESJ-1667


(In reply to Luke Woodward from comment #0)
> 
> I have filed a bug report with Oracle to change the behaviour of these two
> classes.  However, until this gets fixed (if it gets fixed at all), it would
> be appreciated if the JreMemoryLeakPreventionListener could be adapted to
> handle these two classes.

Do you have a bug number for your report?
Comment 4 Luke Woodward 2015-10-26 22:36:37 UTC
(In reply to Konstantin Kolinko from comment #3)
> I raised this issue with Apache Xerces,
> https://issues.apache.org/jira/browse/XERCESJ-1667
> 
> 
> (In reply to Luke Woodward from comment #0)
> > 
> > I have filed a bug report with Oracle to change the behaviour of these two
> > classes.  However, until this gets fixed (if it gets fixed at all), it would
> > be appreciated if the JreMemoryLeakPreventionListener could be adapted to
> > handle these two classes.
> 
> Do you have a bug number for your report?

No, I don't.  I have a Review ID, JI-9025281, but that's all I've heard from Oracle so far.
Comment 5 Luke Woodward 2016-02-22 11:54:30 UTC
I have a bug number for the report now: JDK-8146961.