简体   繁体   English

LDAP PermGen 内存泄漏

[英]LDAP PermGen memory leak

Whenever I use LDAP in a web application it causes a classloader leak, and the strange thing is profilers don't find any GC roots.每当我在 Web 应用程序中使用 LDAP 时,它都会导致类加载器泄漏,奇怪的是分析器找不到任何 GC 根。

I've created a simple web application that demonstrates the leak, it only includes this class:我创建了一个简单的 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();
        }
    }
}

The source code is available here .源代码可在此处获得 I'm using a public LDAP test server for this example, so it should work for everyone if you want to try it.我在这个例子中使用了一个公共 LDAP 测试服务器,所以如果你想尝试它,它应该适用于每个人。 I tried it with the latest JDK 7 and 8 and Tomcat 7 and 8 with the same result – when I click on Reload in Tomcat Web Application Manager and then on Find leaks, Tomcat reports that there's a leak and profilers confirm it.我使用最新的 JDK 7 和 8 以及 Tomcat 7 和 8 进行了尝试,结果相同——当我单击 Tomcat Web 应用程序管理器中的重新加载,然后单击查找泄漏时,Tomcat 报告存在泄漏并且分析器确认了它。

The leak is barely noticeable in this example, but it causes OutOfMemory in a big web application.在这个例子中,泄漏几乎没有引起注意,但它会导致大型 Web 应用程序中的 OutOfMemory。 I didn't find any open JDK bugs about it.我没有发现任何关于它的开放 JDK 错误。

UPDATE 1更新 1

I've tried to use Jetty 9.2 instead of Tomcat and I still see the leak, so it's not Tomcat's fault.我尝试使用 Jetty 9.2 而不是 Tomcat,但我仍然看到泄漏,所以这不是 Tomcat 的错。 Either it's a JDK bug or I'm doing something wrong.要么是 JDK 错误,要么是我做错了什么。

UPDATE 2更新 2

Even though my example demonstrates the leak, it doesn't demonstrate the out of memory error, because it has very small PermGen footprint.即使我的示例演示了泄漏,它也没有演示内存不足错误,因为它的永久代占用空间非常小。 I've created another branch that should be able to reproduce OutOfMemoryError.我创建了 另一个应该能够重现 OutOfMemoryError 的 分支 I just added Spring, Hibernate and Logback dependencies to the project to increase PermGen consumption.我只是在项目中添加了 Spring、Hibernate 和 Logback 依赖项,以增加 PermGen 的消耗。 These dependencies have nothing to do with the leak and I could have used any others instead.这些依赖项与泄漏无关,我可以使用任何其他依赖项。 The only purpose of those is to make PermGen consumption big enough to be able to get OutOfMemoryError.这些的唯一目的是使 PermGen 消耗足够大,以便能够获得 OutOfMemoryError。

Steps to reproduce OutOfMemoryError:重现 OutOfMemoryError 的步骤:

  1. Download or clone the outofmemory-demo branch .下载或克隆outofmemory-demo 分支

  2. Make sure you have JDK 7 and any version of Tomcat and Maven (I used the latest versions - JDK 1.7.0_79 and Tomcat 8.0.26).确保你有 JDK 7 和任何版本的 Tomcat 和 Maven(我使用了最新版本 - JDK 1.7.0_79 和 Tomcat 8.0.26)。

  3. Decrease PermGen size to be able to see the error after the first reload.减小 PermGen 大小以便在第一次重新加载后能够看到错误。 Create setenv.bat (Windows) or setenv.sh (Linux) in Tomcat's bin directory and add set "JAVA_OPTS=-XX:PermSize=24m -XX:MaxPermSize=24m" (Windows) or export "JAVA_OPTS=-XX:PermSize=24m -XX:MaxPermSize=24m" (Linux).在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. Go to Tomcat's conf directory, open tomcat-users.xml and add <role rolename="manager-gui"/><user username="admin" password="1" roles="manager-gui"/> inside <tomcat-users></ tomcat-users> to be able to use Tomcat Web Application Manager.转到Tomcat的conf目录,开放的tomcat-users.xml中添加<role rolename="manager-gui"/><user username="admin" password="1" roles="manager-gui"/>里面<tomcat-users></ tomcat-users>才能使用Tomcat Web Application Manager。

  5. Go to project's directory and use mvn package to build a .war.转到项目目录并使用mvn package构建一个 .war。

  6. Go to Tomcat's webapps directory, delete everything except the manager directory and copy the .war here.进入 Tomcat 的 webapps 目录,删除除 manager 目录之外的所有内容,并将 .war 复制到这里。

  7. Run Tomcat's start script (bin\\startup.bat or bin/startup.sh) and open http://localhost:8080/manager/ , use username admin and password 1.运行 Tomcat 的启动脚本(bin\\startup.bat 或 bin/startup.sh)并打开http://localhost:8080/manager/ ,使用用户名 admin 和密码 1。

  8. Click on Reload and you should see java.lang.OutOfMemoryError: PermGen space in Tomcat's console.单击 Reload,您应该会在 Tomcat 的控制台中看到 java.lang.OutOfMemoryError: PermGen space。

  9. Stop Tomcat, open project's source file src\\main\\java\\org\\example\\LDAPLeakDemo.java , remove the useLDAP();停止Tomcat,打开项目源文件src\\main\\java\\org\\example\\LDAPLeakDemo.java ,去掉useLDAP(); call and save it.调用并保存它。

  10. Repeat steps 5-8, only this time there's no OutOfMemoryError, because the LDAP code is never called.重复步骤 5-8,只是这次没有 OutOfMemoryError,因为 LDAP 代码从未被调用。

First of all: Yes, the LDAP API provided by Sun/Oracle can trigger ClassLoader leaks.首先:是的,Sun/Oracle 提供的LDAP API 可以触发ClassLoader 泄漏。 It is on my list of known offenders , because if system property com.sun.jndi.ldap.connect.pool.timeout is > 0 com.sun.jndi.ldap.LdapPoolManager will spawn a new thread running in the web app that first invoked LDAP.它在我的已知罪犯列表中,因为如果系统属性com.sun.jndi.ldap.connect.pool.timeout > 0 com.sun.jndi.ldap.LdapPoolManager将产生一个在 web 应用程序中运行的新线程,首先调用 LDAP。

That being said, I added your example code as a test case in my ClassLoader Leak Prevention library , so that I'd get an automatic heap dump of the leak.话虽如此,我在ClassLoader Leak Prevention library 中添加了您的示例代码作为测试用例,以便我获得泄漏的自动堆转储。 According to my analysis, there is in fact no leak in your code, however it does seem to take more than one Garbage Collector cycle to get the ClassLoader in question GC:ed (probably due to transient references - haven't dug into it that much).根据我的分析,实际上您的代码中没有泄漏,但是似乎需要不止一个垃圾收集器周期才能获得有问题的类加载器 GC:ed(可能是由于瞬态引用 - 还没有深入研究它很多)。 This probably tricks Tomcat into believing there is a leak, even if there is none.这可能会诱使 Tomcat 相信存在泄漏,即使没有泄漏。

However , since you say you eventually get an OutOfMemoryError , either I'm wrong or there is something else in your app causing these leaks.但是,既然您说您最终会收到OutOfMemoryError ,那么要么是我错了,要么是您的应用程序中有其他原因导致了这些泄漏。 If you add my ClassLoader Leak Prevention library to your app, does it still leak/cause OOME s?如果您将我的 ClassLoader Leak Prevention 库添加到您的应用程序中,它是否仍然泄漏/导致OOME s? Does the Preventor log any warnings?预防器是否记录任何警告?

If you set up your application server to create a heap dump whenever there is an OOME , you can look for the leak using Eclipse Memory Analyzer.如果您将应用程序服务器设置为在出现OOME时创建堆转储,则可以使用 Eclipse Memory Analyzer 查找泄漏。 I've explained the process in detail here .我已经在这里详细解释了这个过程。

It's been a while since I posted this question.我发布这个问题已经有一段时间了。 I finally found what really happened, so I thought I post it as the answer in case @MattiasJiderhamn or others are interested.我终于找到了真正发生的事情,所以我想我把它作为答案发布,以防@MattiasJiderhamn 或其他人感兴趣。

The reason profilers didn't find any GC roots was because JVM was hiding the java.lang.Throwable.backtrace field as described in https://bugs.openjdk.java.net/browse/JDK-8158237 .分析器没有找到任何 GC 根的原因是因为 JVM 隐藏了java.lang.Throwable.backtrace字段,如https://bugs.openjdk.java.net/browse/JDK-8158237 中所述 Now that this limitation is gone I was able to get the GC root:现在这个限制消失了,我能够获得 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

The cause of this leak is the LDAP implementation in JDK.此泄漏的原因是 JDK 中的 LDAP 实现。 The com.sun.jndi.ldap.LdapCtx class has a static filed com.sun.jndi.ldap.LdapCtx类有一个静态文件

private static final HierMemDirCtx EMPTY_SCHEMA = new HierMemDirCtx();

com.sun.jndi.toolkit.dir.HierMemDirCtx contains the readOnlyEx field that is assigned to an instance of javax.naming.directory.SchemaViolationException during the LDAP initialization that happens after the new InitialDirContext(env) call in the code from my question. com.sun.jndi.toolkit.dir.HierMemDirCtx包含在 LDAP 初始化期间分配给javax.naming.directory.SchemaViolationException实例的readOnlyEx字段,该实例在我的问题中的代码中调用new InitialDirContext(env)之后发生。 The issue is java.lang.Throwable , which is the superclass of all exceptions including javax.naming.directory.SchemaViolationException , has the backtrace field.问题是java.lang.Throwable ,它是所有异常的超类,包括javax.naming.directory.SchemaViolationException ,具有backtrace字段。 This field contains references to all classes in the stacktrace at the time the constructor was called, including my own org.example.LDAPLeakDemo class, which in turn holds a reference to the web application classloader.此字段包含对调用构造函数时堆栈跟踪中所有类的引用,包括我自己的org.example.LDAPLeakDemo类,该类又包含对 Web 应用程序类加载器的引用。

Here's a similar leak that was fixed in Java 9 https://bugs.openjdk.java.net/browse/JDK-8146961这是 Java 9 中修复的类似泄漏https://bugs.openjdk.java.net/browse/JDK-8146961

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM