上一篇讲了如何使用MAT分析类加载引起的内存泄露问题
这一次,我们将讨论导致泄漏的不同原因,查看第三方库中的一个泄漏示例,并了解如何通过解决方法修复该泄漏。
类加载器泄漏的不同原因
为了知道在堆转储分析中应该寻找什么,我们可以将类加载器泄漏分为三种不同的类型。最后,它们都只是第一个的变体。
- 来自webapp外部的引用(即来自应用服务器或JDK类)指向类加载器本身或它已加载的某个类(后者又引用了类加载器),包括此类类的任何实例。
- 在你的webapp中运行的线程。如果您从web应用程序中生成可能不会终止的新线程,它们很可能会阻止类加载器被垃圾回收。即使线程不使用webapps类加载器加载的任何类,也可能发生这种情况。这是因为线程有一个上下文类加载器,在java.lang.Thread class。在下一篇文章中会有更多的相关信息。
- 具有值的ThreadLocals,其类已加载到webapp中。如果在webapp中使用ThreadLocals,则需要在webapp关闭之前显式清除所有ThreadLocals。这是因为a)应用程序服务器使用线程池,这意味着该线程将比您的webapp实例长;b)ThreadLocal值实际上存储在java.lang.Thread对象。因此,这只是1的变化。(注意:这种情况很可能是您自己创建的,但也存在于第三方库中。)
来自应用程序外部的引用示例
在试图查找web应用程序中的类加载器泄漏时,我创建了一个小JSP页面,在该页面中我循环了应用程序的所有第三方jar。我尝试加载在自定义类加载器中找到的每个类,向类加载器添加ZombieMarker(参见上一篇文章),然后释放该类加载器。我一遍又一遍地运行JSP页面,直到得到一个java.lang.OutOfMemoryError:PermGen
空间。也就是说,我可以通过从第三方库加载类来触发类加载器泄漏…事实证明,引发这种行为的不止一种。
以下是其中一个的MAT trace:
(在这张图片中,我们的类加载器在哪里并不明显。自定义类加载器是JSP中的匿名内部类,因此它是第二个以$1结尾的奇怪类名的条目。)
乍一看,这看起来像是上面的类型2,带有一个正在运行的线程。但是情况并非如此,因为线程本身不是GC根(而不是底层)。事实上,有一个线程参与,但它没有运行。
相反,我们可以看到,阻止类装入器被垃圾收集的是来自Web应用程序外部的引用(java.lang*)到的实例com.sun.media.jai.codec.TempFileCleanupThread
,然后由类加载器加载。从引用和引用的名称(java.lang.applicationshdownlook
)类,我怀疑JVM关闭钩子是在加载时被一些Java高级映像(JAI)类添加的。
这个com.sun.media.jai.codec.TempFileCleanupThread
类位于JAI的编解码器部分;在我们的示例中,是1.1.2_u01版本。这些来源可在官方SVN回购(1.1.2 U 01标签)中找到。正如你所见,TempFileCleanupThread.java类不在该列表中。这是因为有人认为将它作为一个包保护类放在Fil中是一个好主意ecacheSeKablestream.java.
在那里我们也可以找到泄漏的来源。
// Create the cleanup thread. Use reflection to preserve compile-time
// compatibility with JDK 1.2.
static {
try {
Method shutdownMethod =
Runtime.class.getDeclaredMethod("addShutdownHook",
new Class[] {Thread.class});
cleanupThread = new TempFileCleanupThread();
shutdownMethod.invoke(Runtime.getRuntime(),
new Object[] {cleanupThread});
} catch(Exception e) {
// Reset the Thread to null if Method.invoke failed.
cleanupThread = null;
}
}
正如所怀疑的那样,有一个静态块(通过反射)添加了一个JVM关闭钩子,只要com.sun.media.jai.codec.FileCacheSeekableStream
类已加载。在web应用程序环境中不太实用,因为JVM在应用程序服务器关闭之前不会关闭。
JAI TempFileCleanupThread应该在JVM关闭时删除临时文件。在web应用程序中,我们需要的可能是在重新部署web应用程序后立即删除这些临时文件。如果这是我们自己的代码,我们应该改变它。在本例中,它是一个第三方库,从SVN主干来看,这个问题仍然没有得到修复,所以升级没有帮助。
在重新部署时清理泄漏的引用
为了在web应用程序关闭过程中清理引用,防止类加载器泄漏,有两种方法。您可以将代码放入启动时加载的Servlet的destroy()方法中
<servlet servlet-name='cleanup' servlet-class='my.CleanupServlet'>
<load-on-startup>1</load-on-startup>
</servlet>
或者(可能稍微更正确)您可以创建javax.servlet.ServletContextListener并将清理添加到contextDemounded()方法。
<listener>
<listener-class>my.CleanupListener</listener-class>
</listener>
解决方法
幸运的是,在我们的例子中,FileCacheSeekableStream保留了对shutdown钩子的引用。
public final class FileCacheSeekableStream extends SeekableStream {
/** A thread to clean up all temporary files on VM exit (VM 1.3+) */
private static TempFileCleanupThread cleanupThread = null;
所以让我们抓住那个参照物并移除关闭钩子。但我们可能不只是想扔掉钩子,因为从理论上讲,这可能会给我们留下一些临时文件,这些文件应该在JVM关闭时被删除。取而代之的是,取下钩子,然后立即运行。
实际上,我们可以将其转换为一个通用方法,用于我们希望移除的其他第三方关闭钩子。(系统输出用于日志记录,因为日志框架通常也需要清理,我建议您在调用此方法之前进行清理。)
private static void removeShutdownHook(Class clazz, String field) {
// Note that loading the class may add the hook if not yet present...
try {
// Get the hook
final Field cleanupThreadField = clazz.getDeclaredField(field);
cleanupThreadField.setAccessible(true);
Thread cleanupThread = (Thread) cleanupThreadField.get(null);
if(cleanupThread != null) {
// Remove hook to avoid PermGen leak
System.out.println(" Removing " + cleanupThreadField + " shutdown hook");
Runtime.getRuntime().removeShutdownHook(cleanupThread);
// Run cleanup immediately
System.out.println(" Running " + cleanupThreadField + " shutdown hook");
cleanupThread.start();
cleanupThread.join(60 * 1000); // Wait up to 1 minute for thread to run
if(cleanupThread.isAlive())
System.out.println("STILL RUNNING!!!");
else
System.out.println("Done");
}
else
System.out.println(" No " + cleanupThreadField + " shutdown hook");
}
catch (NoSuchFieldException ex) {
System.err.println("*** " + clazz.getName() + '.' + field +
" not found; has JAR been updated??? ***");
ex.printStackTrace();
}
catch(Exception ex) {
System.err.println("Unable to unregister " + clazz.getName() + '.' + field);
ex.printStackTrace();
}
}
现在我们只需在应用程序关闭中调用该方法(清除服务。销毁() / CleanupListener.contextDestroyed())是这样的:
removeShutdownHook(com.sun.media.jai.codec.FileCacheSeekableStream.class,
"cleanupThread");
在最坏的情况下,如果没有对shutdown钩子的引用,我们可以在JVM类中使用反射。它看起来像这样:
final Field field =
Class.forName("java.lang.ApplicationShutdownHooks").getDeclaredField("hooks");
field.setAccessible(true);
Map<Thread, Thread> shutdownHooks = (Map<Thread, Thread>) field.get(null);
// Iterate copy to avoid ConcurrentModificationException
for(Thread t : new ArrayList<Thread>(shutdownHooks.keySet())) {
if(t.getClass().getName().equals("class.name.of.ShutdownHook")) { // TODO: Set name
// Make sure it's from this web app instance
if(t.getClass().getClassLoader().equals(this.getClass().getClassLoader())) {
Runtime.getRuntime().removeShutdownHook(t); // Remove hook to avoid PermGen leak
t.start(); // Run cleanup immediately
t.join(60 * 1000); // Wait up to 1 minute for thread to run
}
}
}
这篇文章到此为止。下一次我们将研究在类加载器中运行的线程。
更新–Bean验证API请求“FIXME”
我忍不住又发了一个例子,这是我前几天发现的。在一个新的webapp中出现了一些PermGen错误,这是我发现的:
//cache per classloader for an appropriate discovery
//keep them in a weak hashmap to avoid memory leaks and allow proper hot redeployment
//TODO use a WeakConcurrentHashMap
//FIXME The List<VP> does keep a strong reference to the key ClassLoader, use the same model as JPA CachingPersistenceProviderResolver
private static final Map<ClassLoader, List<ValidationProvider<?>>> providersPerClassloader =
new WeakHashMap<ClassLoader, List<ValidationProvider<?>>>();
这不是很好吗?在BeanValidationAPI(JSR303)——不是实现,而是API——有一个缓存是在考虑热重新部署的情况下创建的,但它仍然有可能泄漏类加载器。不仅如此,代码的作者已经意识到它可以泄漏类加载器,而且仍然是validation-api-1.0.0。盖尔被释放了,没有任何手动通知缓存释放类加载器的方法。唉…
当API随应用服务器一起提供时,就会触发泄漏,但实现(在我的例子中是Hibernate Validator)是在web应用程序中提供的,因此使用类加载器加载。
使用类似于上面的反射,我们通过获取Map并remove()我们的类加载器来阻止泄漏。或者,我们可以在应用服务器级别添加验证提供者的JAR,这样缓存就不会引用我们的webapp类加载器。
Java8中的元空间Metaspace及其示例:https://javakk.com/436.html
除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/466.html
暂无评论