LDAP PermGen内存泄漏

时间:2015-09-28 16:50:26

标签: java memory-leaks ldap classloader permgen

每当我在Web应用程序中使用LDAP时,都会导致类加载器泄漏,奇怪的是分析器找不到任何GC根。

我创建了一个简单的Web应用程序来演示泄漏,它只包括这个类:

@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应用程序管理器中单击重新加载然后在查找泄漏时,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并在<role rolename="manager-gui"/><user username="admin" password="1" roles="manager-gui"/>中添加<tomcat-users></ tomcat-users>以便能够使用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. 点击Reload,你会在Tomcat的控制台中看到java.lang.OutOfMemoryError:PermGen空间。

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

  10. 重复步骤5-8,只是这次没有OutOfMemoryError,因为从不调用LDAP代码。

2 个答案:

答案 0 :(得分:1)

首先:是的,Sun / Oracle提供的LDAP API可以触发ClassLoader泄漏。它在my list of known offenders上,因为如果系统属性com.sun.jndi.ldap.LdapPoolManager是&gt; 0 OutOfMemoryError将生成在首次调用LDAP的Web应用程序中运行的新线程。

话虽这么说,我在你的ClassLoader Leak Prevention library中添加了你的示例代码作为测试用例,这样我就可以获得泄漏的自动堆转储。根据我的分析,你的代码实际上没有泄漏,但是它似乎需要多个垃圾收集器周期来获得有问题的ClassLoader GC:ed(可能是由于瞬态引用 - 没有挖到它许多)。这可能会诱使Tomcat相信存在泄漏,即使没有泄漏。

然而,因为你说你最终得到OOME,要么我错了,要么你的应用中还有其他东西导致这些泄漏。如果您将my ClassLoader Leak Prevention library添加到您的应用中,它是否仍会泄漏/导致OOME?预防者是否记录任何警告?

如果设置应用程序服务器以在存在IS NULL时创建堆转储,则可以使用Eclipse Memory Analyzer查找泄漏。我已经详细解释了这个过程here

答案 1 :(得分:0)

我发布此问题已经有一段时间了。我终于找到了真正发生的事情,所以我认为我将其作为答案发布,以防@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

此泄漏的原因是JDK中的LDAP实现。 com.sun.jndi.ldap.LdapCtx类有静态字段

private static final HierMemDirCtx EMPTY_SCHEMA = new HierMemDirCtx();

com.sun.jndi.toolkit.dir.HierMemDirCtx包含在LDAP初始化期间分配给readOnlyEx实例的javax.naming.directory.SchemaViolationException字段,该字段在我的问题代码中new InitialDirContext(env)调用之后发生。问题是java.lang.Throwable,它是所有例外的超类,包括javax.naming.directory.SchemaViolationException,具有backtrace字段。该字段包含对构造函数被调用时stacktrace中所有类的引用,包括我自己的org.example.LDAPLeakDemo类,后者依次保存对Web应用程序类加载器的引用。

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

中修复的类似泄漏