JDK-6717784 : Memory leak in LogManager when used with subclassed resource bundle
  • Type: Bug
  • Component: core-libs
  • Sub-Component: java.util.logging
  • Affected Version: 6
  • Priority: P4
  • Status: Closed
  • Resolution: Duplicate
  • OS: windows_2003
  • CPU: x86
  • Submitted: 2008-06-23
  • Updated: 2011-02-16
  • Resolved: 2008-06-25
Related Reports
Duplicate :  
Description
FULL PRODUCT VERSION :
java version "1.6.0_06"
Java(TM) SE Runtime Environment (build 1.6.0_06-b02)
Java HotSpot(TM) Client VM (build 10.0-b22, mixed mode, sharing)

ADDITIONAL OS VERSION INFORMATION :
Microsoft Windows [Version 5.2.3790]

A DESCRIPTION OF THE PROBLEM :
LogManager retains strong references to Loggers.
Logger retains strong reference to ResourceBundle.
When ResourceBundle is subclassed and loaded by a custom class loader, this results in a memory leak because custom class loader is strongly reachable from LogManager.
In environments with dynamic class loaders (such as J2EE) this often leads to infamous OutOfMemoryError: PermGen space exception.
The problem was discovered with Glassfish v2 and Apache MyFaces Trinidad 1.2.8. Trinidad subclasses ListResourceBundle to provide log messages localization.


STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Execute the provided test case.


EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Both test methods should succeed.
ACTUAL -
Test method test1() fails.
Test method test2() succeeds.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------

======= MyClassLoader.java =======
import java.net.URLClassLoader;
import java.net.URL;

public class MyClassLoader extends URLClassLoader
{
    public MyClassLoader(URL[] urls)
    {
        super(urls, null);
    }

    public static MyClassLoader create()
    {
        URLClassLoader defaultLoader = (URLClassLoader) Thread.currentThread().getContextClassLoader();
        URL[] classpath = defaultLoader.getURLs();
        return new MyClassLoader(classpath);
    }
}

======= MyLogger.java =======
import java.util.logging.Logger;

public class MyLogger extends Logger
{
    public MyLogger(String name, String resourceBundleName)
    {
        super(name, resourceBundleName);
    }
}

======= MyResourceBundle1.java =======
import java.util.ListResourceBundle;

public class MyResourceBundle1 extends ListResourceBundle
{
    protected Object[][] getContents()
    {
        return new Object[][]
                {
                        {"test.key", "test value 1"},
                };
    }
}

======= Test1.java =======
import static junit.framework.Assert.*;
import org.junit.Test;

import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.ResourceBundle;
import java.util.logging.LogManager;
import java.util.logging.Logger;

public class Test1
{
    @Test
    public void test1() throws Exception
    {
        // init log manager in default class loader
        LogManager.getLogManager();

        ClassLoader myClassLoader = MyClassLoader.create();
        createLoggerWithTempClassLoader(myClassLoader, "test1", "MyResourceBundle1");

        assertSame(myClassLoader, bundleClassLoader(Logger.getLogger("test1").getResourceBundle()));
        assertEquals("test value 1", Logger.getLogger("test1").getResourceBundle().getString("test.key"));

        myClassLoader = null;
        System.gc();

        assertNull(bundleClassLoader(Logger.getLogger("test1").getResourceBundle()));
    }

    @Test
    public void test2() throws Exception
    {
        // init log manager in default class loader
        LogManager.getLogManager();

        ClassLoader myClassLoader = MyClassLoader.create();
        createLoggerWithTempClassLoader(myClassLoader, "test2", "MyResourceBundle2");

        assertSame(myClassLoader, bundleClassLoader(Logger.getLogger("test2").getResourceBundle()));
        assertEquals("test value 2", Logger.getLogger("test2").getResourceBundle().getString("test.key"));

        myClassLoader = null;
        System.gc();

        assertNull(bundleClassLoader(Logger.getLogger("test2").getResourceBundle()));
    }

    private void createLoggerWithTempClassLoader(ClassLoader myClassLoader, String loggerName, String bundleName)
    {
        ClassLoader defaultLoader = Thread.currentThread().getContextClassLoader();
        Thread.currentThread().setContextClassLoader(myClassLoader);
        MyLogger logger = new MyLogger(loggerName, bundleName);
        assertTrue(LogManager.getLogManager().addLogger(logger));
        Thread.currentThread().setContextClassLoader(defaultLoader);
    }

    private ClassLoader bundleClassLoader(ResourceBundle resourceBundle) throws Exception
    {
        Object cacheKey = getFieldValue(ResourceBundle.class, resourceBundle, "cacheKey");
        WeakReference loaderRef = (WeakReference) getFieldValue(cacheKey, "loaderRef");
        return (ClassLoader) loaderRef.get();
    }

    private Object getFieldValue(Object obj, String fieldName) throws Exception
    {
        return getFieldValue(obj.getClass(), obj, fieldName);
    }

    private Object getFieldValue(Class clazz, Object obj, String fieldName) throws Exception
    {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);
    }
}


---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Do not use ResourceBundle subclassing with Java Logging API.

Comments
EVALUATION This CR is a dup of the CR that has been fixed in JDK 7: 6274920 JDK logger holds strong reference to java.util.logging.Logger instances The fix of 6274920 replaced strong references to loggers with WeakReferences in the loggers HashTable.
25-06-2008