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,在<tomcat-users></ tomcat-users>
中添加<role rolename="manager-gui"/><user username="admin" password="1" roles="manager-gui"/>
,即可使用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 添加到您的应用程序中,它是否仍然泄漏/导致OOME
s? 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 内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章