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 的步骤:

  1. 下载或克隆 outofmemory-demo branch.

  2. 确保你有 JDK 7 和任何版本的 Tomcat 和 Maven(我使用的是最新版本 - JDK 1.7.0_79和 Tomcat 8.0.26).

  3. 减小 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).

  4. 进入Tomcat的conf目录,打开tomcat-users.xml,在<tomcat-users></ tomcat-users>里面添加<role rolename="manager-gui"/><user username="admin" password="1" roles="manager-gui"/>即可使用 Tomcat Web 应用程序管理器。

  5. 进入项目目录,使用mvn package构建.war.

  6. 进入Tomcat的webapps目录,删除manager目录以外的所有内容,复制.war这里

  7. 运行 Tomcat的启动脚本(bin\startup.bat或bin/startup.sh)并打开http://localhost:8080/manager/,使用用户名 admin 和密码 1.

  8. 单击重新加载,您应该会在 Tomcat 的控制台中看到 java.lang.OutOfMemoryError: PermGen space。

  9. 停止Tomcat,打开项目的源文件src\main\java\org\example\LDAPLeakDemo.java,移除useLDAP();调用并保存。

  10. 重复步骤 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] 的引用=]装载机.

这是在 Java 9 https://bugs.openjdk.java.net/browse/JDK-8146961

中修复的类似漏洞