Утечка памяти в LDAP PermGen

Всякий раз, когда я использую LDAP в веб-приложении, это вызывает утечку загрузчика классов, и странно то, что профилировщики не находят никаких корней GC.

Я создал простое веб-приложение, которое демонстрирует утечку, оно включает только этот класс:

@WebListener
public class LDAPLeakDemo implements ServletContextListener {
    public void contextInitialized(ServletContextEvent sce) { 
        useLDAP();
    }

    public void contextDestroyed(ServletContextEvent sce) {}

    private void useLDAP() {
        Hashtable<String, Object> env = new Hashtable<String, Object>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, "ldap://ldap.forumsys.com:389");
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, "cn=read-only-admin,dc=example,dc=com");
        env.put(Context.SECURITY_CREDENTIALS, "password");
        try {
            DirContext ctx = null;
            try {
                ctx = new InitialDirContext(env);
                System.out.println("Created the initial context");
            } finally {
                if (ctx != null) {
                    ctx.close(); 
                    System.out.println("Closed the context");
                }
            }
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

Исходный код доступен здесь. Я использую общедоступный тестовый сервер LDAP для этого примера, поэтому он должен работать для всех, если вы хотите попробовать его. Я попробовал его с последними версиями JDK 7 и 8 и Tomcat 7 и 8 с тем же результатом - когда я нажимаю "Перезагрузить" в Tomcat Web Application Manager, а затем "Найти утечки", Tomcat сообщает, что есть утечка, и профилировщики подтверждают это.

В этом примере утечка едва заметна, но она вызывает OutOfMemory в большом веб-приложении. Я не нашел открытых ошибок JDK по этому поводу.

ОБНОВЛЕНИЕ 1

Я попытался использовать Jetty 9.2 вместо Tomcat, и все еще вижу утечку, так что это не ошибка Tomcat. Либо это ошибка JDK, либо я делаю что-то не так.

ОБНОВЛЕНИЕ 2

Несмотря на то, что мой пример демонстрирует утечку, он не демонстрирует ошибку нехватки памяти, потому что у него очень маленькая занимаемая площадь PermGen. Я создал еще одну ветку, которая должна воспроизводить OutOfMemoryError. Я просто добавил в проект зависимости Spring, Hibernate и Logback, чтобы увеличить потребление PermGen. Эти зависимости не имеют ничего общего с утечкой, и я мог бы использовать другие. Единственная цель этого - сделать потребление PermGen достаточно большим, чтобы можно было получить OutOfMemoryError.

Шаги для воспроизведения OutOfMemoryError:

  1. Скачайте или клонируйте ветку outofmemory-demo.

  2. Убедитесь, что у вас JDK 7 и любая версия Tomcat и Maven (я использовал последние версии - JDK 1.7.0_79 и Tomcat 8.0.26).

  3. Уменьшите размер PermGen, чтобы увидеть ошибку после первой перезагрузки. Создайте setenv.bat (Windows) или setenv.sh (Linux) в каталоге bin Tomcat и добавьте set "JAVA_OPTS=-XX:PermSize=24m -XX:MaxPermSize=24m" (Windows) или export "JAVA_OPTS=-XX:PermSize=24m -XX:MaxPermSize=24m" (Linux).

  4. Перейдите в каталог conf Tomcat, откройте tomcat-users.xml и добавьте <role rolename="manager-gui"/><user username="admin" password="1" roles="manager-gui"/> внутри <tomcat-users></ tomcat-users> чтобы иметь возможность использовать Tomcat Web Application Manager.

  5. Перейти в каталог проекта и использовать mvn package построить.war.

  6. Перейдите в каталог веб-приложений Tomcat, удалите все, кроме каталога менеджера, и скопируйте сюда.war.

  7. Запустите сценарий запуска Tomcat (bin\startup.bat или bin/startup.sh) и откройте http://localhost:8080/manager/, используйте имя пользователя admin и пароль 1.

  8. Нажмите "Перезагрузить", и вы должны увидеть java.lang.OutOfMemoryError: пространство PermGen в консоли Tomcat.

  9. Остановите Tomcat, откройте исходный файл проекта src\main\java\org\example\LDAPLeakDemo.java, удалить useLDAP(); позвони и сохрани.

  10. Повторите шаги 5-8, только в этот раз OutOfMemoryError отсутствует, поскольку код LDAP никогда не вызывается.

2 ответа

Решение

Прошло много времени с тех пор, как я разместил этот вопрос. Я наконец нашел то, что действительно произошло, поэтому я решил опубликовать это как ответ на случай, если @MattiasJiderhamn или другие заинтересуются.

Профилировщики не нашли корни GC потому, что JVM скрывала java.lang.Throwable.backtrace поле, как описано в https://bugs.openjdk.java.net/browse/JDK-8158237. Теперь, когда это ограничение исчезло, я смог получить корень GC:

this     - value: org.apache.catalina.loader.WebappClassLoader #2
 <- <classLoader>     - class: org.example.LDAPLeakDemo, value: org.apache.catalina.loader.WebappClassLoader #2
  <- [10]     - class: java.lang.Object[], value: org.example.LDAPLeakDemo class LDAPLeakDemo
   <- [2]     - class: java.lang.Object[], value: java.lang.Object[] #3394
    <- backtrace     - class: javax.naming.directory.SchemaViolationException, value: java.lang.Object[] #3386
     <- readOnlyEx     - class: com.sun.jndi.toolkit.dir.HierMemDirCtx, value: javax.naming.directory.SchemaViolationException #1
      <- EMPTY_SCHEMA (sticky class)     - class: com.sun.jndi.ldap.LdapCtx, value: com.sun.jndi.toolkit.dir.HierMemDirCtx #1

Причиной этой утечки является реализация LDAP в JDK. com.sun.jndi.ldap.LdapCtx класс имеет статическое поле

private static final HierMemDirCtx EMPTY_SCHEMA = new HierMemDirCtx();

com.sun.jndi.toolkit.dir.HierMemDirCtx содержит readOnlyEx поле, назначенное экземпляру javax.naming.directory.SchemaViolationException во время инициализации LDAP, которая происходит после new InitialDirContext(env) позвоните в код из моего вопроса. Вопрос java.lang.Throwable, который является суперклассом всех исключений, включая javax.naming.directory.SchemaViolationException, имеет backtrace поле. Это поле содержит ссылки на все классы в stacktrace во время вызова конструктора, включая мой собственный org.example.LDAPLeakDemo класс, который в свою очередь содержит ссылку на загрузчик классов веб-приложения.

Вот аналогичная утечка, которая была исправлена ​​в Java 9 https://bugs.openjdk.java.net/browse/JDK-8146961

Прежде всего: да, API LDAP, предоставляемый Sun/Oracle, может вызывать утечки ClassLoader. Это в моем списке известных нарушителей, потому что, если системное свойство com.sun.jndi.ldap.connect.pool.timeout > 0 com.sun.jndi.ldap.LdapPoolManager создаст новый поток, работающий в веб-приложении, которое первым вызвало LDAP.

При этом я добавил ваш пример кода в качестве контрольного примера в мою библиотеку ClassLoader Leak Prevention, чтобы получить автоматический дамп кучи утечки. Согласно моему анализу, на самом деле в вашем коде нет утечки, однако, кажется, что требуется более одного цикла сборщика мусора, чтобы получить ClassLoader в вопросе GC:ed (возможно, из-за временных ссылок - не вникнул в это, что много). Это, вероятно, обманывает Tomcat, заставляя поверить, что есть утечка, даже если ее нет.

Тем не менее, так как вы говорите, что в конечном итоге вы получите OutOfMemoryError, либо я ошибаюсь, либо в вашем приложении есть что-то еще, что вызывает эти утечки. Если вы добавите мою библиотеку ClassLoader Leak Prevention в ваше приложение, она все еще будет иметь утечку / вызвать OOME s? Preventor регистрирует какие-либо предупреждения?

Если вы настроили свой сервер приложений для создания дампа кучи, когда есть OOME Вы можете найти утечку, используя Eclipse Memory Analyzer. Я объяснил процесс подробно здесь.

Другие вопросы по тегам