Утечка памяти в 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:
Скачайте или клонируйте ветку outofmemory-demo.
Убедитесь, что у вас JDK 7 и любая версия Tomcat и Maven (я использовал последние версии - JDK 1.7.0_79 и Tomcat 8.0.26).
Уменьшите размер 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).Перейдите в каталог 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.Перейти в каталог проекта и использовать
mvn package
построить.war.Перейдите в каталог веб-приложений Tomcat, удалите все, кроме каталога менеджера, и скопируйте сюда.war.
Запустите сценарий запуска Tomcat (bin\startup.bat или bin/startup.sh) и откройте http://localhost:8080/manager/, используйте имя пользователя admin и пароль 1.
Нажмите "Перезагрузить", и вы должны увидеть java.lang.OutOfMemoryError: пространство PermGen в консоли Tomcat.
Остановите Tomcat, откройте исходный файл проекта
src\main\java\org\example\LDAPLeakDemo.java
, удалитьuseLDAP();
позвони и сохрани.Повторите шаги 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. Я объяснил процесс подробно здесь.