FULL PRODUCT VERSION :
java version "1.8.0_60"
Java(TM) SE Runtime Environment (build 1.8.0_60-b27)
Java HotSpot(TM) 64-Bit Server VM (build 25.60-b23, mixed mode)
ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows [Version 10.0.10240]
(This issue relates to code in Java classes in rt.jar, so I don't believe the OS or OS version is important here. Still, that's the version I'm using right now.)
A DESCRIPTION OF THE PROBLEM :
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 each of these two classes is statically initialized, its 'abort' exception is created, and its stacktrace is filled in. If a class in a web application is unfortunate enough to be in the call stack when this happens, that class can no longer be garbage collected as the exception's stack trace now contains a reference to this class. Consequently, this class, and all of the other classes loaded by the web app's classloader, cannot be removed from PermGen space within the JVM. This causes a memory leak.
I would suggest the following fix:
(1): Create a custom subclass of RuntimeException, which overrides fillInStackTrace to just return this. Use an instance of this subclass instead of a RuntimeException for DOMNormalizer.abort. (This appears to be the approach taken for two other static final fields which also contain exceptions: com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.END_OF_DOCUMENT_ENTITY and com.sun.org.apache.xerces.internal.parsers.AbstractDOMParser$Abort.INSTANCE. Neither of these exceptions causes the same problem.)
(2): Delete the field com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl.abort, as it appears to be unused.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
This assumes you have an Apache Tomcat web server, as it makes use of Tomcat's memory-leak diagnostics:
1. Create a WAR file with the servlet class and web.xml detailed below.
2. Deploy the WAR file to a Tomcat application server.
3. View the index.html page generated by the servlet. (This should display the text 'done' in the browser window.)
4. Use the Tomcat manager to reload the web application.
5. Click 'Find leaks' in the Diagnostics section of the Tomcat manager.
6. Use JVisualVM (or another profiler) to look in the Tomcat application for instances of org.apache.catalina.loader.WebappClassLoader with a 'state' of 'DESTROYED'.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
At step 5, Tomcat reports no memory leaks. At step 6, there are no destroyed Tomcat classloader instances, i.e. the classloader used for the application that was reloaded has now been garbage collected.
ACTUAL -
At step 5, Tomcat reports a memory leak, naming the web application created. At step 6, JVisualVM finds one destroyed Tomcat classloader, which is being kept alive because of one of the two 'abort' exceptions described above.
ERROR MESSAGES/STACK TRACES THAT OCCUR :
Here's the path from the destroyed webapp classloader to one of the 'abort' exceptions, obtained using 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
(The class is named DOMNormalizerLeakServlet as I initially noticed this issue with DOMNormalizer in a real app. The test case code happens to trigger the situation with DOMSerializerImpl instead. It may be possible to adjust the test case code to trigger it with DOMNormalizer.abort instead, e.g. by normalizing the document created before serializing.)
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
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);
}
}
}
<?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>
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Add the names of the two classes com.sun.org.apache.xerces.internal.dom.DOMNormalizer and com.sun.org.apache.xml.internal.serialize.DOMSerializerImpl to the classesToInitialize parameter of the JreMemoryLeakPreventionListener in TOMCAT_HOME/conf/server.xml.