LDAP PermGen 内存泄漏
LDAP PermGen memory leak
每当我在 Web 应用程序中使用 LDAP 时,它都会导致 class加载程序泄漏,奇怪的是分析器找不到任何 GC 根。
我创建了一个简单的 Web 应用程序来演示泄漏,它只包含这个 class:
@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. I’m using a public LDAP test server 可用,因此如果您想尝试,它应该适用于所有人。
我用最新的 JDK 7 和 8 以及 Tomcat 7 和 8 进行了尝试,结果相同——当我在 Tomcat Web 应用程序管理器中单击“重新加载”,然后单击“查找泄漏”时,Tomcat 报告存在泄漏,分析人员确认了这一点。
在此示例中泄漏几乎不明显,但它会在大型 Web 应用程序中导致 OutOfMemory。我没有发现任何关于它的开放 JDK 错误。
更新 1
我尝试使用 Jetty 9.2 而不是 Tomcat,但我仍然看到泄漏,所以这不是 Tomcat 的错。要么是 JDK 错误,要么是我做错了什么。
更新 2
尽管我的示例演示了泄漏,但并未演示内存不足错误,因为它的 PermGen 占用空间非常小。我创建了 another branch 应该能够重现 OutOfMemoryError。我刚刚向项目添加了 Spring、Hibernate 和 Logback 依赖项以增加 PermGen 消耗。这些依赖项与泄漏无关,我本可以使用任何其他依赖项。这些的唯一目的是使 PermGen 消耗足够大以便能够获得 OutOfMemoryError。
重现 OutOfMemoryError 的步骤:
下载或克隆 outofmemory-demo branch.
确保你有 JDK 7 和任何版本的 Tomcat 和 Maven(我使用的是最新版本 - JDK 1.7.0_79和 Tomcat 8.0.26).
减小 PermGen 的大小以便在第一次重新加载后能够看到错误。在Tomcat的bin目录下创建setenv.bat(Windows)或setenv.sh(Linux)并添加set "JAVA_OPTS=-XX:PermSize=24m -XX:MaxPermSize=24m"
(Windows)或 export "JAVA_OPTS=-XX:PermSize=24m -XX:MaxPermSize=24m"
(Linux).
进入Tomcat的conf目录,打开tomcat-users.xml,在<tomcat-users></ tomcat-users>
里面添加<role rolename="manager-gui"/><user username="admin" password="1" roles="manager-gui"/>
即可使用 Tomcat Web 应用程序管理器。
进入项目目录,使用mvn package
构建.war.
进入Tomcat的webapps目录,删除manager目录以外的所有内容,复制.war这里
运行 Tomcat的启动脚本(bin\startup.bat或bin/startup.sh)并打开http://localhost:8080/manager/,使用用户名 admin 和密码 1.
单击重新加载,您应该会在 Tomcat 的控制台中看到 java.lang.OutOfMemoryError: PermGen space。
停止Tomcat,打开项目的源文件src\main\java\org\example\LDAPLeakDemo.java
,移除useLDAP();
调用并保存。
重复步骤 5-8,只是这次没有 OutOfMemoryError,因为从未调用过 LDAP 代码。
首先:是的,Sun/Oracle 提供的 LDAP API 可以触发 ClassLoader 泄漏。它在 my list of known offenders 上,因为如果系统 属性 com.sun.jndi.ldap.connect.pool.timeout
> 0 com.sun.jndi.ldap.LdapPoolManager
将在首次调用 LDAP 的 Web 应用程序中生成一个新线程 运行。
也就是说,我在我的 ClassLoader Leak Prevention library 中添加了您的示例代码作为测试用例,这样我就可以获得泄漏的自动堆转储。根据我的分析,您的代码实际上没有泄漏,但是似乎需要不止一个垃圾收集器周期才能获得有问题的 ClassLoader GC:ed(可能是由于瞬态引用 - 尚未深入研究就这么多)。这可能会让 Tomcat 相信存在泄漏,即使存在 none.
但是,既然你说你最终得到一个 OutOfMemoryError
,要么我错了,要么你的应用程序中有其他东西导致了这些泄漏。如果您将 my ClassLoader Leak Prevention library 添加到您的应用程序,它仍然是 leak/cause OOME
吗? Preventor 是否记录任何警告?
如果将应用程序服务器设置为在出现 OOME
时创建堆转储,则可以使用 Eclipse Memory Analyzer 查找泄漏。我已经详细解释了这个过程here。
我 post 已经有一段时间没回答这个问题了。我终于找到了真正发生的事情,所以我想我 post 它作为答案,以防@MattiasJiderhamn 或其他人感兴趣。
探查器没有找到任何 GC 根的原因是 JVM 隐藏了 java.lang.Throwable.backtrace
字段,如 https://bugs.openjdk.java.net/browse/JDK-8158237 中所述。现在这个限制消失了,我能够获得 GC root:
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
此泄漏的原因是 JDK 中的 LDAP 实现。 com.sun.jndi.ldap.LdapCtx
class 有一个静态字段
private static final HierMemDirCtx EMPTY_SCHEMA = new HierMemDirCtx();
com.sun.jndi.toolkit.dir.HierMemDirCtx
包含分配给 javax.naming.directory.SchemaViolationException
实例的 readOnlyEx
字段,在我的问题代码中 new InitialDirContext(env)
调用之后发生的 LDAP 初始化期间.问题是 java.lang.Throwable
,它是包括 javax.naming.directory.SchemaViolationException
在内的所有异常的超级 class,具有 backtrace
字段。此字段包含对调用构造函数时堆栈跟踪中所有 classes 的引用,包括我自己的 org.example.LDAPLeakDemo
class,它又包含对 Web 应用程序 [=35] 的引用=]装载机.
中修复的类似漏洞
每当我在 Web 应用程序中使用 LDAP 时,它都会导致 class加载程序泄漏,奇怪的是分析器找不到任何 GC 根。
我创建了一个简单的 Web 应用程序来演示泄漏,它只包含这个 class:
@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. I’m using a public LDAP test server 可用,因此如果您想尝试,它应该适用于所有人。 我用最新的 JDK 7 和 8 以及 Tomcat 7 和 8 进行了尝试,结果相同——当我在 Tomcat Web 应用程序管理器中单击“重新加载”,然后单击“查找泄漏”时,Tomcat 报告存在泄漏,分析人员确认了这一点。
在此示例中泄漏几乎不明显,但它会在大型 Web 应用程序中导致 OutOfMemory。我没有发现任何关于它的开放 JDK 错误。
更新 1
我尝试使用 Jetty 9.2 而不是 Tomcat,但我仍然看到泄漏,所以这不是 Tomcat 的错。要么是 JDK 错误,要么是我做错了什么。
更新 2
尽管我的示例演示了泄漏,但并未演示内存不足错误,因为它的 PermGen 占用空间非常小。我创建了 another branch 应该能够重现 OutOfMemoryError。我刚刚向项目添加了 Spring、Hibernate 和 Logback 依赖项以增加 PermGen 消耗。这些依赖项与泄漏无关,我本可以使用任何其他依赖项。这些的唯一目的是使 PermGen 消耗足够大以便能够获得 OutOfMemoryError。
重现 OutOfMemoryError 的步骤:
下载或克隆 outofmemory-demo branch.
确保你有 JDK 7 和任何版本的 Tomcat 和 Maven(我使用的是最新版本 - JDK 1.7.0_79和 Tomcat 8.0.26).
减小 PermGen 的大小以便在第一次重新加载后能够看到错误。在Tomcat的bin目录下创建setenv.bat(Windows)或setenv.sh(Linux)并添加
set "JAVA_OPTS=-XX:PermSize=24m -XX:MaxPermSize=24m"
(Windows)或export "JAVA_OPTS=-XX:PermSize=24m -XX:MaxPermSize=24m"
(Linux).进入Tomcat的conf目录,打开tomcat-users.xml,在
<tomcat-users></ tomcat-users>
里面添加<role rolename="manager-gui"/><user username="admin" password="1" roles="manager-gui"/>
即可使用 Tomcat Web 应用程序管理器。进入项目目录,使用
mvn package
构建.war.进入Tomcat的webapps目录,删除manager目录以外的所有内容,复制.war这里
运行 Tomcat的启动脚本(bin\startup.bat或bin/startup.sh)并打开http://localhost:8080/manager/,使用用户名 admin 和密码 1.
单击重新加载,您应该会在 Tomcat 的控制台中看到 java.lang.OutOfMemoryError: PermGen space。
停止Tomcat,打开项目的源文件
src\main\java\org\example\LDAPLeakDemo.java
,移除useLDAP();
调用并保存。重复步骤 5-8,只是这次没有 OutOfMemoryError,因为从未调用过 LDAP 代码。
首先:是的,Sun/Oracle 提供的 LDAP API 可以触发 ClassLoader 泄漏。它在 my list of known offenders 上,因为如果系统 属性 com.sun.jndi.ldap.connect.pool.timeout
> 0 com.sun.jndi.ldap.LdapPoolManager
将在首次调用 LDAP 的 Web 应用程序中生成一个新线程 运行。
也就是说,我在我的 ClassLoader Leak Prevention library 中添加了您的示例代码作为测试用例,这样我就可以获得泄漏的自动堆转储。根据我的分析,您的代码实际上没有泄漏,但是似乎需要不止一个垃圾收集器周期才能获得有问题的 ClassLoader GC:ed(可能是由于瞬态引用 - 尚未深入研究就这么多)。这可能会让 Tomcat 相信存在泄漏,即使存在 none.
但是,既然你说你最终得到一个 OutOfMemoryError
,要么我错了,要么你的应用程序中有其他东西导致了这些泄漏。如果您将 my ClassLoader Leak Prevention library 添加到您的应用程序,它仍然是 leak/cause OOME
吗? Preventor 是否记录任何警告?
如果将应用程序服务器设置为在出现 OOME
时创建堆转储,则可以使用 Eclipse Memory Analyzer 查找泄漏。我已经详细解释了这个过程here。
我 post 已经有一段时间没回答这个问题了。我终于找到了真正发生的事情,所以我想我 post 它作为答案,以防@MattiasJiderhamn 或其他人感兴趣。
探查器没有找到任何 GC 根的原因是 JVM 隐藏了 java.lang.Throwable.backtrace
字段,如 https://bugs.openjdk.java.net/browse/JDK-8158237 中所述。现在这个限制消失了,我能够获得 GC root:
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
此泄漏的原因是 JDK 中的 LDAP 实现。 com.sun.jndi.ldap.LdapCtx
class 有一个静态字段
private static final HierMemDirCtx EMPTY_SCHEMA = new HierMemDirCtx();
com.sun.jndi.toolkit.dir.HierMemDirCtx
包含分配给 javax.naming.directory.SchemaViolationException
实例的 readOnlyEx
字段,在我的问题代码中 new InitialDirContext(env)
调用之后发生的 LDAP 初始化期间.问题是 java.lang.Throwable
,它是包括 javax.naming.directory.SchemaViolationException
在内的所有异常的超级 class,具有 backtrace
字段。此字段包含对调用构造函数时堆栈跟踪中所有 classes 的引用,包括我自己的 org.example.LDAPLeakDemo
class,它又包含对 Web 应用程序 [=35] 的引用=]装载机.