JDK-6916498 : Class loader leak goes unreported via JVMTI
  • Type: Bug
  • Component: hotspot
  • Sub-Component: gc
  • Affected Version: hs14.3
  • Priority: P3
  • Status: Closed
  • Resolution: Won't Fix
  • OS: solaris_8
  • CPU: x86
  • Submitted: 2010-01-13
  • Updated: 2016-12-15
  • Resolved: 2013-09-05
Description
FULL PRODUCT VERSION :
Java(TM) SE Runtime Environment (build 1.6.0_17-b04)
Java HotSpot(TM) 64-Bit Server VM (build 14.3-b01, mixed mode

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows [Version 5.2.3790]
Windows XP, 64-bit, SP2

EXTRA RELEVANT SYSTEM CONFIGURATION :
Not applicable

A DESCRIPTION OF THE PROBLEM :
The context class loader during the first call to DocumentBuilderFactory.newInstance().newDocumentBuilder() is pinned into memory but the details of what is pinning the class loader are not available via the JVMTI interface.

This frequently occurs in servlet containers and can result in a significant memory leak on web application reload.

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Execute the code provided below with whatever settings are required by your profiler of choice (I used YourKit)
2. Connect the profiler
3. Force GC
4. Check the instances of URLClassLoader present
5. Trigger a heap dump
6. Trace the GC roots of any URLClassLoader

EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The expected result when a profiler is attached is that, after forcing GC, there are zero instances of URLClassLoader present.
ACTUAL -
The actual result when a profiler is attached is that, after forcing GC, there is  one instance of URLClassLoader present. No GC Roots are reported for this object (checked with multiple profilers including JHat) but the object is never GC'd. This creates a memory leak.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
package org.apache.markt;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.lang.reflect.Method;
import java.net.URLClassLoader;

public final class ClassLoaderLeak {
  public static void main(final String[] args) throws Exception {
    createLoader();

    System.in.read(); // capture snapshot at this point
  }

  public static void foo() {
    try {
      DocumentBuilderFactory.newInstance().newDocumentBuilder();
    }
    catch (ParserConfigurationException ignored) {
        // Ignore
    }
  }

  private static void createLoader() throws Exception {
    final ClassLoader myLoader = new URLClassLoader(
      ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs(),
      null
    );
    // load some class
    final Class<?> aClass = myLoader.loadClass(ClassLoaderLeak.class.getName());
    final Method method = aClass.getDeclaredMethod("foo", new Class<?>[]{});
    method.invoke(null, new Object[]{});
  }
}
---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
The workaround is to force DocumentBuilderFactory.newInstance().newDocumentBuilder() to be called from a class loader that is not expected to be GC'd.

Comments
Good background. Since this is verified to be fixed in JDK7, closing the bug as Won't Fix for JDK6.
05-09-2013

Since this is a GC bug and is not affecting JDK 7 or 8, I am changing the fix version to 6, and reassign to gc team. Not sure if "6-pool" is correct. Please update as needed.
17-07-2013

This bug seems to affect only JDK 6. JDK 7 and 8 seem OK: I have modified the original test case (ClassLoaderLeak2.java) so that we can check whether the URLClassLoader is GC'ed without using a heap dump. ---------------------------------------------------------------- package org.apache.markt; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.lang.reflect.Method; import java.net.URLClassLoader; import java.util.Vector; public final class ClassLoaderLeak2 { static int exhaust() { int n = 0; Vector v = new Vector(); try { while (true) { v.addElement(new byte[64000]); n++; } } catch (Throwable t) {} return n; } public static void main(final String[] args) throws Exception { createLoader(); if (args.length > 0 && "exhaust".equals(args[0])) { long n = exhaust(); System.out.println("exhaust = " + (n * 64000)); } else { System.gc(); } try { Thread.sleep(1000); } catch (Throwable t) {} if (args.length <= 1) { System.out.println("Failed to collect MyURLClassLoader"); System.exit(1); } else { System.in.read(); // capture snapshot at this point } } public static void foo() { try { DocumentBuilderFactory.newInstance().newDocumentBuilder(); } catch (ParserConfigurationException ignored) { // Ignore } } static class MyURLClassLoader extends URLClassLoader { MyURLClassLoader(java.net.URL[] urls) { super(urls, null); } public void finalize() { System.out.println("MyURLClassLoader is collected"); System.exit(0); } } private static void createLoader() throws Exception { final ClassLoader myLoader = new MyURLClassLoader( ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs()); // load some class final Class<?> aClass = myLoader.loadClass(ClassLoaderLeak2.class.getName()); final Method method = aClass.getDeclaredMethod("foo", new Class<?>[]{}); method.invoke(null, new Object[]{}); } } ---------------------------------------------------------------- $ alias java6=/net/koori/onestop/jdk/6_26/promoted/latest/binaries/linux-i586/bin/java $ alias java7=/net/koori/onestop/jdk/7_40/promoted/latest/binaries/linux-i586/bin/java $ alias java8=/net/koori/onestop/jdk/8/promoted/latest/binaries/linux-i586/bin/java $ java6 -version java version "1.6.0_26" Java(TM) SE Runtime Environment (build 1.6.0_26-b03) Java HotSpot(TM) Server VM (build 20.1-b02, mixed mode) $ java6 -cp classes -Xmx256m -XX:+UseSerialGC org.apache.markt.ClassLoaderLeak2 exhaust exhaust = 259200000 Failed to collect MyURLClassLoader $ java6 -cp classes -Xmx256m -XX:+UseParallelGC org.apache.markt.ClassLoaderLeak2 exhaust exhaust = 240192000 Failed to collect MyURLClassLoader $ java6 -cp classes -Xmx256m -XX:+UseConcMarkSweepGC org.apache.markt.ClassLoaderLeak2 exhaust exhaust = 264768000 Failed to collect MyURLClassLoader $ java6 -cp classes -Xmx256m -XX:+UseParallelOldGC org.apache.markt.ClassLoaderLeak2 exhaust exhaust = 238272000 Failed to collect MyURLClassLoader $ java7 -version java version "1.7.0_40-ea" Java(TM) SE Runtime Environment (build 1.7.0_40-ea-b33) Java HotSpot(TM) Server VM (build 24.0-b52, mixed mode) $ java7 -cp classes -Xmx256m -XX:+UseSerialGC org.apache.markt.ClassLoaderLeak2 exhaust MyURLClassLoader is collected $ java7 -cp classes -Xmx256m -XX:+UseParallelGC org.apache.markt.ClassLoaderLeak2 exhaust MyURLClassLoader is collected $ java7 -cp classes -Xmx256m -XX:+UseConcMarkSweepGC org.apache.markt.ClassLoaderLeak2 exhaust MyURLClassLoader is collected $ java7 -cp classes -Xmx256m -XX:+UseParallelOldGC org.apache.markt.ClassLoaderLeak2 exhaust MyURLClassLoader is collected $ java8 -version java version "1.8.0-ea" Java(TM) SE Runtime Environment (build 1.8.0-ea-b98) Java HotSpot(TM) Server VM (build 25.0-b40, mixed mode) $ java8 -cp classes -Xmx256m -XX:+UseSerialGC org.apache.markt.ClassLoaderLeak2 exhaust MyURLClassLoader is collected $ java8 -cp classes -Xmx256m -XX:+UseParallelGC org.apache.markt.ClassLoaderLeak2 exhaust MyURLClassLoader is collected $ java8 -cp classes -Xmx256m -XX:+UseConcMarkSweepGC org.apache.markt.ClassLoaderLeak2 exhaust MyURLClassLoader is collected $ java8 -cp classes -Xmx256m -XX:+UseParallelOldGC org.apache.markt.ClassLoaderLeak2 exhaust MyURLClassLoader is collected
17-07-2013

To echo Dan - first make sure there actually is a problem.
17-07-2013