购物网站主页模版,论文引用网站怎样做脚注,创意的广告图片,建设摩托车官网的网站首页在前面的文章中#xff0c;我们分析了 tomcat 类加载器的相关源码#xff0c;也了解了 tomcat 支持类的热加载#xff0c;意味着 tomcat 要涉及类的重复卸装/装载过程#xff0c;这个过程是很敏感的#xff0c;一旦处理不当#xff0c;可能会引起内存泄露
卸载类
我们知…在前面的文章中我们分析了 tomcat 类加载器的相关源码也了解了 tomcat 支持类的热加载意味着 tomcat 要涉及类的重复卸装/装载过程这个过程是很敏感的一旦处理不当可能会引起内存泄露
卸载类
我们知道class 信息存放在元数据区1.7是 Perm 区这一块的内存相比堆而言只占据非常小的空间但是如果处理不当还是有可能会导致内存溢出。这让我回想起几年前的一个故障线上环境启用了 tomcat 的自动 reload 功能出现过 java.lang.OutOfMemoryError: PermGen space 问题排查的结果是因为 tomcat 在自动重载应用的时候没有正常卸载类导致 Perm 区内存没能被释放而发生溢出。tomcat 会尽量避免这类问题的发生但是不能百分之百保证不会出现所以还是建议不要随意开启 reloadable 功能
卸载类的条件很苛刻必须同时满足以下3点 1、 该类所有的实例已经被回收 2、 加载该类的 ClassLoder 已经被回收 3、 该类对应的 java.lang.Class 对象没有任何地方被引用
针对第1点保证所有的实例被回收这点不难tomcat 在 Context 组件中实例化这些对象持有直接或间接的引用所以在热部署的时候只要回收 Context 组件即可保证实例对象被回收。
在前面的文章中我们分析了 tomcat 类加载器tomcat 使用 ParallelWebappClassLoader 加载 Class在热部署的时候自然也会回收该类加载器。但是要注意的是ParallelWebappClassLoader 会作为线程上下文的类加载器因此要避免该类加载器对象在其他地方被引用。其实这个问题是最隐晦的jdk 中有些类会持有线程上下文的类加载器作为一个优秀的开源产品tomcat 为我们解决了很多诸如此类的问题
此外还要保证类对应的 java.lang.Class 对象没有任何地方引用只要 Class 对象作用域限制在 Context 组件的作用范围便不会发生泄露tomcat 也是这么做了使用 Context 实现了隔离机制
热加载问题
热加载会面临很多问题有很多坑需要非常丰富的经验。下面针对 tomcat 中涉及的类加载器泄露、对象泄露、文件锁等这几类常见的问题加以分析讨论。如果您对热加载感兴趣的话可以研究下阿里开源的 jarlinks
文件锁
在 Windows 系统下使用 URLConnection 读取本地 jar 包的资源时它会将资源缓存起来会导致该 jar 包资源被锁。如果这个时候使用 war 包进行重新部署需要解压 war 包再把原来目录下面的 jar 包删除由于 jar 包资源被锁导致删除失败重新部署自然也会失败。我们先来看一段代码这段代码会抛出异常java.nio.file.FileSystemException: E:\spring-boot-2.0.1.RELEASE.jar: 另一个程序正在使用此文件进程无法访问说明该 jar 包被锁了
String path E://spring-boot-2.0.1.RELEASE.jar;
File file new File( path );
URL url file.toURI().toURL();URLConnection uConn url.openConnection();
uConn.getLastModified(); // 读取jar包信息为了解决文件锁的问题tomcat 禁用了 URLConnection 的缓存是在 JreMemoryLeakPreventionListener 中完成的关键代码如下所示
// dummy.jar 不存在也没有关系
URL url new URL(jar:file://dummy.jar!/);
URLConnection uConn url.openConnection();
可能有些童鞋会有疑问tomcat 只是针对该 URLConnection 对象禁用了缓存而其它的 URLConnection 资源缓存未必被禁用啊。答案是肯定的因为 URLConnection 的 defaultUseCaches 属性是静态变量
类加载器泄露
其中一种 JRE 内存泄露是因为上下文类加载器导致的内存泄露。某些 JRE 库以单例的形式存在它的生命周期很长甚至会贯穿于整个 java 程序它们会使用上下文类加载器加载类并且保留了类加载器的引用所以会导致被引用的类加载器无法被回收而 tomcat 重加载 webapp 是创建一个新的类加载器来实现的旧的类加载器无法被 gc 回收致使其加载的 Class 也无法被回收导致内存泄露。
DriverManager 就是典型的例子它利用 jdk 提供的 SPI 机制加载 java.sql.Driver 驱动而 jdk 提供的 SPI 机制便是使用上下文类加载器加载 Class 的如果这类 jdbc 驱动由 ParallelWebappClassLoader 类加载器加载的话就会导致该 ClassLoder 无法被回收自然会出现内存泄露
我们来看看 tomcat 是怎么解决的tomcat 是利用 LifecycleListener 处理 before_init 事件将上下文类加载器置为系统类加载器并且完成驱动的加载过程最后为了不影响其它的类加载再将上下文类加载器重置为 ParallelWebappClassLoader
另外一种 JRE 内存泄露是因为当前线程会启动另外一个线程这个时候新线程会引用当前线程的上下文类加载器如果新线程无止尽地运行那么上下文类加载器就会一直被引用而无法被回收导致内存泄露。sun.awt.AppContext.getAppContext() 便是典型的例子它会在内部开启一个 AWT-AppKit 线程直到图形化环境准备就绪例如 ImageIO.getCacheDirectory()、java.awt.Toolkit.getDefaultToolkit() 针对这种情况解决思路也是一样的只需要将当前上下文类加载器指定为系统类加载器即可关键代码如下所示
JreMemoryLeakPreventionListener.javaOverride
public void lifecycleEvent(LifecycleEvent event) {if (Lifecycle.BEFORE_INIT_EVENT.equals(event.getType())) {ClassLoader loader Thread.currentThread().getContextClassLoader();try {// 当线程上下文类加载器指定为系统类加载器Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());if (driverManagerProtection) {DriverManager.getDrivers();}// 避免开启的子线程持有 ParallelWebappClassLoader 引用if (appContextProtection !JreCompat.isJre8Available()) {ImageIO.getCacheDirectory();}if (awtThreadProtection !JreCompat.isJre9Available()) {java.awt.Toolkit.getDefaultToolkit();}// 避免持有 ParallelWebappClassLoader 引用if (tokenPollerProtection !JreCompat.isJre9Available()) {java.security.Security.getProviders();}// 忽略若干代码......} finally {// 再重置为 ParallelWebappClassLoader避免影响其它的类的加载Thread.currentThread().setContextClassLoader(loader);}}
ThreadLocal 对象泄露
还有一种内存泄露是由于 ThreadLocal 引起的假如我们在 ThreadLocal 中保存了对象A而且对象A由 ParallelWebappClassLoader 加载那么就可以看成线程引用了对象A。由于 tomcat 中处理请求的是线程池意味着该线程会存活很长一段时间。webapp 热加载时会重新实例化一个 ParallelWebappClassLoader 对象如果线程未销毁那么旧的 ParallelWebappClassLoader 也无法被回收导致内存泄露。
解决 ThreadLocal 内存泄露最好的办法自然是把线程池中的所有的线程销毁并重新创建。这个过程分为两步第一步是将任务队列堵住不让新的任务进来第二步是将线程池中所有线程停止。
tomcat 解决该 ThreadLocal 对象泄露问题也是借助了 Lifecycle 完成的具体的实现类是 ThreadLocalLeakPreventionListener它会处理 Lifecycle.AFTER_STOP_EVENT 事件并且销毁线程池内的空闲线程关键代码如下所示