java内存泄漏本质是对象被gc roots(如静态变量、线程局部变量)强引用而无法回收,常见场景包括静态集合无限增长、未关闭资源、监听器未注销、threadlocal未清理及classloader泄漏;解决方案涵盖使用weakhashmap、try-with-resources、显式remove()、urlclassloader隔离及容器协同卸载。

做不到“完美”不漏内存,但可以大幅降低泄漏风险。关键不在“销毁”动作本身——JVM 不提供主动卸载 ClassLoader 的 API——而在于让旧 ClassLoader 及其加载的所有类,彻底满足被垃圾回收的条件。
确保旧 ClassLoader 完全不可达
ClassLoader 能被回收的前提是:它自身、它加载的所有类、这些类创建的所有实例,全部不再被任何 GC Roots 引用。
- 显式清理静态字段:检查所有 static final Logger、static Map、static ExecutorService 等,调用其 close()、shutdown() 或 clear();避免在静态上下文中持有业务类实例
- 清空线程局部变量(ThreadLocal):每个使用了 ThreadLocal 的类,在卸载前必须调用 remove(),否则 ThreadLocal 的 value 会强引用旧类加载器
- 停止所有后台线程:Timer、ScheduledExecutor、自定义守护线程等,必须在取消部署时显式 shutdown,并等待终止完成
- 解除第三方库隐式引用:Log4j、Jackson、Hibernate 等常通过 static 持有 ClassLoader 上下文,查阅对应文档启用“上下文清理”或配置隔离模式
使用独立、无父委托的 URLClassLoader
复用或错误继承系统类加载器,会导致新旧类混杂、引用链难以切断。应明确切断与上层加载器的关联。
- 构造时传入 null 作为 parent:new URLClassLoader(urls, null),避免委托给 AppClassLoader
- 不要重写 loadClass(),只覆盖 findClass(),保证双亲委派逻辑可控且不破坏基础类(如 java.lang.*)的加载一致性
- 每次热更新都新建一个 ClassLoader 实例,绝不复用;旧实例在清理后立即置为 null
- 加载类后,仅通过反射操作实例(如 clazz.getDeclaredConstructor().newInstance()),避免编译期硬编码类型导致隐式持旧类引用
配合容器行为,触发真正卸载时机
单靠代码清理还不够,需与运行环境协同。Tomcat、WebLogic 等容器有自己的生命周期管理策略。
- Tomcat 中,确保 context.xml 设置
reloadable="true"且关闭 JSP 自动编译缓存(development="false"),减少元数据残留 - WebLogic 控制台中,将“旧版本保留数”设为 0,“重新部署超时”设为合理值(如 30 秒),避免旧部署长期挂起
- 调用标准取消部署命令(如
weblogic.Deployer -undeploy或 Tomcat Manager 的/manager/text/undeploy),而非直接替换 WAR 文件 - 观察日志中是否出现 “Stopping service [Catalina]”、“Unloading web application” 类提示,确认容器已执行完整卸载流程
验证与监控不能省略
没有验证的清理等于没做。内存泄漏往往延迟暴露,必须建立可观测性。
- 定期打印类加载器层次:
Thread.currentThread().getContextClassLoader()向上遍历,确认旧加载器实例已不可见 - 监控 Metaspace 使用量:通过
ManagementFactory.getMemoryPoolMXBeans()查看 “Metaspace” 池的 used / committed,多次热部署后不应持续上涨 - 触发 Full GC 后用 jcmd 或 jmap 检查:jcmd
VM.class_hierarchy -all | grep YourAppPackage,确认旧版本类已消失 - 生产环境禁用热部署;开发/测试环境用 JProfiler、VisualVM 连接,实时查看 ClassLoader 实例数和 retained heap











