LDAP PermGen 内存泄漏

Posted

技术标签:

【中文标题】LDAP PermGen 内存泄漏【英文标题】:LDAP PermGen memory leak 【发布时间】:2015-12-25 23:49:57 【问题描述】:

每当我在 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 的步骤:

    下载或克隆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,在&lt;tomcat-users&gt;&lt;/ tomcat-users&gt;中添加&lt;role rolename="manager-gui"/&gt;&lt;user username="admin" password="1" roles="manager-gui"/&gt;,即可使用Tomcat Web Application Manager。

    进入项目目录并使用mvn package构建一个.war。

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

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

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

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

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

【问题讨论】:

Tomcat 究竟报告了什么? @EJP 它显示了它的标准泄漏消息——“以下 Web 应用程序已停止(重新加载、取消部署),但它们之前运行的类仍加载到内存中,从而导致内存泄漏(使用探查器确认)”。 那是因为之前的条件。你需要向我们展示这一点。从长远来看,将类保留在内存中并不是任何类型的内存泄漏。这既正常又必不可少。你不断增长的记忆问题出在其他地方。 @EJP 你在说什么先决条件?此示例项目仅包含我的问题中的类。您可以从我提供的链接下载该项目并复制它。我知道垃圾收集器并不总是收集旧的类加载器,但如果 PermGen 中没有足够的空间,它必须收集它以避免内存不足错误。否则它会因为泄漏而无法收集。 【参考方案1】:

自从我发布这个问题以来已经有一段时间了。我终于找到了真正发生的事情,所以我想我把它作为答案发布,以防@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 包含readOnlyEx 字段,该字段在我的问题代码中的new InitialDirContext(env) 调用之后发生的LDAP 初始化期间分配给javax.naming.directory.SchemaViolationException 的实例。问题是java.lang.Throwable,它是包括javax.naming.directory.SchemaViolationException 在内的所有异常的超类,具有backtrace 字段。此字段包含调用构造函数时对堆栈跟踪中所有类的引用,包括我自己的 org.example.LDAPLeakDemo 类,该类又包含对 Web 应用程序类加载器的引用。

这是在 Java 9 https://bugs.openjdk.java.net/browse/JDK-8146961 中修复的类似泄漏

【讨论】:

【参考方案2】:

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

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

然而,既然你说你最终会得到一个OutOfMemoryError,要么我错了,要么你的应用程序中有其他东西导致了这些泄漏。如果您将my ClassLoader Leak Prevention library 添加到您的应用程序中,它是否仍然泄漏/导致OOMEs? Preventor 是否记录任何警告?

如果您将应用程序服务器设置为在有OOME 时创建堆转储,则可以使用 Eclipse 内存分析器查找泄漏。我已经详细解释了这个过程here。

【讨论】:

感谢您对此进行调查。我试过你的图书馆,但不幸的是它没有帮助,也没有记录任何警告。我尝试使用 Eclipse MAT,但结果与分析器相同——没有对类加载器的强引用和软引用,但 GC 永远无法收集它。泄漏仅在第一次重新部署后发生,但类加载器永远留在内存中,无论我重新部署多少次或在分析器中使用“Perform GC”。我更新了我的问题并添加了一个实际产生 OutOfMemoryError 的 webapp 以及重现它的步骤。如果您能尝试一下,我将不胜感激。 不确定我何时/是否有时间测试您的示例。同时:必须始终有足够的 PermGen 来保存应用程序的至少 2 个副本,因为在加载第二个副本之前通常不会对第一个副本进行垃圾收集。因此,如果您在第一次重新部署时获得 OOME,那么您的设置太低而无法进行有意义的测试。如果你可以重新部署一次,强制 GC(System.gc() 直到 WeakReference 为空)然后让它在下一次重新部署时崩溃,那么可能是一个真正的问题。 它只泄漏一次,所以如果我将 PermGen 的大小设置得足够大,我就不会得到 OOME。但这仍然是一个漏洞,因为我可以将 PermGen 的大小设置为略大于容纳 2 个副本所需的大小,我可以多次重新部署并在分析器中查看 GC 如何努力清理内存,但永远无法收集第一个类加载器。强制 GC 也无济于事。如果我只是删除useLDAP() 调用,我就不会再看到这种行为了。当然,我可以忍受这种泄漏,但如果没有强引用和软引用,并且有办法防止它,那么知道导致它的原因会很好。 我会尝试另一个垃圾收集器,看看行为是否保持不变。例如-XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled。另外,你愿意分享一个堆转储给我看吗...? -XX:+UseConcMarkSweepGC -XX:+CMSClassUnloadingEnabled 没有帮助。我已经上传了转储here。为了得到这个转储,我遵循了我的问题中的步骤 1-8,但还在步骤 3 中添加了-XX:+HeapDumpOnOutOfMemoryError

以上是关于LDAP PermGen 内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

Java 应用程序中加载的类数中可能存在内存泄漏

c++ 内存泄漏问题

MFC内存泄漏调试

如何防止java中的内存泄漏

记录一次DialogFragment 内存泄漏

常见的内存泄漏原因及解决方法