4年前 (2020-09-24)  爪哇岛 |   抢沙发  672 
文章评分 0 次,平均分 0.0

上一篇讲了如何使用MAT分析类加载引起的内存泄露问题

这一次,我们将讨论导致泄漏的不同原因,查看第三方库中的一个泄漏示例,并了解如何通过解决方法修复该泄漏。

类加载器泄漏的不同原因

为了知道在堆转储分析中应该寻找什么,我们可以将类加载器泄漏分为三种不同的类型。最后,它们都只是第一个的变体。

  1. 来自webapp外部的引用(即来自应用服务器或JDK类)指向类加载器本身或它已加载的某个类(后者又引用了类加载器),包括此类类的任何实例。
  2. 在你的webapp中运行的线程。如果您从web应用程序中生成可能不会终止的新线程,它们很可能会阻止类加载器被垃圾回收。即使线程不使用webapps类加载器加载的任何类,也可能发生这种情况。这是因为线程有一个上下文类加载器,在java.lang.Thread class。在下一篇文章中会有更多的相关信息。
  3. 具有值的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:

ClassLoaderLeak类加载泄露-找到不需要的引用

(在这张图片中,我们的类加载器在哪里并不明显。自定义类加载器是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错误,这是我发现的:

ClassLoaderLeak类加载泄露-找到不需要的引用

//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

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册