2015-09-28 6 views
1

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

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

@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(); 
     } 
    } 
} 

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

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

UPDATE 1

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

UPDATE 2

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

Действия по воспроизведению OutOfMemoryError:

  1. Скачать или клонировать outofmemory-demo branch.

  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. Перейдите в каталог confc 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. Перейдите в каталог webapps Tomcat, удалите все, кроме каталога менеджера, и скопируйте здесь .war здесь.

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

  8. Кликните на Reload, и вы должны увидеть java.lang.OutOfMemoryError: пространство PermGen в консоли Tomcat.

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

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

+0

Tomcat сообщает, что именно? – EJP

+0

@EJP Это стандартное сообщение об утечке - «Следующие веб-приложения были остановлены (перезагружены, не развернуты), но их классы из предыдущих запусков по-прежнему загружаются в память, что вызывает утечку памяти (используйте профилировщик для подтверждения)». – John29

+0

Это из-за предшествующего состояния. Вы должны показать нам это. Сохранение классов в памяти - это не утечка памяти в течение длительного времени. Это нормально и существенно. Ваша растущая проблема памяти лежит где-то в другом месте. – EJP

ответ

1

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

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

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

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

+0

Спасибо, что посмотрели на это. Я пробовал вашу библиотеку, но, к сожалению, она не помогает и не регистрирует никаких предупреждений. Я пытался использовать Eclipse MAT, но результат такой же, как и с профайлерами - никаких сильных и мягких ссылок на загрузчик классов, но GC никогда не сможет его собрать. Утечка происходит только после первого перераспределения, но загрузчик классов остается в памяти навсегда, независимо от того, сколько раз я повторно развертываю или использую «Выполнять GC» в профилографах. Я обновил свой вопрос и добавил webapp, который фактически создает OutOfMemoryError и шаги по его воспроизведению. Буду признателен, если вы сможете попробовать. – John29

+0

Не уверен, когда/если я найду время, чтобы проверить ваш пример. Между тем: всегда должно быть достаточно PermGen для хранения не менее 2 копий приложения, так как первая копия обычно не будет собираться с мусором до того, как будет загружена вторая. Поэтому, если вы получаете OOME при первом повторном развертывании, ваша настройка слишком низкая, чтобы быть значимым тестом. Если вы можете перераспределить один раз, принудительно используйте GC (System.gc(), пока не будет проигнорировано значение WeakReference), а затем получите его для сбоя при следующем повторном развертывании, тогда, вероятно, будет реальная проблема. –

+0

Он просачивается только один раз, поэтому, если я установил размер PermGen достаточно большим, я не получу OOME.Но это все еще утечка, потому что я могу установить размер PermGen немного больше, чем то, что требуется для хранения 2-х копий, я могу многократно перераспределять и видеть в профилировщике, как GC пытается очистить память, но никогда не сможет собрать этот первый загрузчик классов. Принудительный GC также не помогает. Если я просто удалю вызов 'useLDAP()', я больше этого не вижу. Конечно, я могу жить с этой утечкой, но было бы хорошо знать, что вызывает ее, если нет сильных и мягких ссылок, и если есть способ ее предотвратить. – John29