环境变量配置本身不会直接导致内存泄漏,但错误设置java_opts或catalina_opts(如混用、重复追加、未清理旧值)会掩盖真实泄漏源;-xms与-xmx差异过大易误判为泄漏,heapdump参数配置不当会干扰监控,参数重复叠加导致jvm异常,调试参数错放引发非预期引用链;真泄漏需结合gc日志(ou持续单向爬升)和jstat验证,热部署引发的classloader泄漏才是高频真凶。

环境变量配置本身不会直接导致内存泄漏,但错误地设置 JAVA_OPTS 或 CATALINA_OPTS(尤其是混用、重复追加、未清理旧值)会掩盖真实泄漏源,甚至让问题更难复现和定位。
为什么改了内存参数后反而“像”有泄漏?
常见错觉来源:
-
-Xms和-Xmx设得差异过大(比如-Xms256m -Xmx2g),JVM 会缓慢扩容堆,GC 日志里看到老年代持续增长,容易被误判为泄漏; - 加了
-XX:+HeapDumpOnOutOfMemoryError却没配-XX:HeapDumpPath,OOM 时 dump 写入临时目录失败,残留大量半截文件,占用磁盘并干扰监控; - 在
setenv.sh中用JAVA_OPTS="$JAVA_OPTS -Xmx2g"多次 source 或多次部署,导致参数重复叠加(如出现-Xmx2g -Xmx2g -Xmx2g),JVM 启动失败或行为异常,日志混乱; - 把本该用
CATALINA_OPTS的调试参数(如-Dcom.sun.management.jmxremote)错塞进JAVA_OPTS,导致子进程(如 JSP 编译器、后台线程)也继承这些参数,引发非预期引用链。
确认是不是真泄漏:先看 catalina.out 里的 GC 行为
不要只盯“内存用了多少”,要看 GC 是否还能回收:
- 如果日志里频繁出现
Full GC (Ergonomics)但每次回收后老年代使用率仍 >95%,且jstat -gc <pid></pid>显示OU(old used)持续单向爬升 → 真泄漏嫌疑高; - 如果
OU偶尔冲高但下一次 Full GC 后回落明显(比如从 1.8g → 0.3g),大概率是缓存堆积或 GC 策略不匹配,不是泄漏; - 注意 JDK 版本:JDK 8u40+ 默认启用元空间(Metaspace),
java.lang.OutOfMemoryError: Metaspace不等于堆泄漏,而是 classloader 未卸载(典型于热部署场景)。
setenv.sh 配置必须检查的三件事
这是最容易出错的实操环节:
- 只用
CATALINA_OPTS放 JVM 参数,JAVA_OPTS留空或仅放-D系统属性 —— 因为CATALINA_OPTS只作用于 Tomcat 主进程,而JAVA_OPTS会被所有子命令(如version.sh)继承; - 避免在脚本里写
CATALINA_OPTS="$CATALINA_OPTS -Xmx2g"这类追加逻辑,改为直接赋值:CATALINA_OPTS="-Xms2g -Xmx2g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC"; - 删掉所有注释掉但未清理的旧参数行(尤其带
-XX:PermSize的 —— JDK 8+ 已废弃,残留会导致启动失败)。
热部署后 ClassLoader 泄漏才是高频真凶
你改完环境变量重启 Tomcat,发现“不泄漏”了,但重新部署一次 WAR 就又卡住?那基本锁定是 WebappClassLoader 没卸载:
- 访问
http://localhost:8080/manager/html→ 点对应应用后的Find leaks按钮,返回SEVERE: The web application [] appears to have started a thread named [...] but has failed to stop it.就是典型征兆; - 根本原因常是:静态持有
ServletContext、未 shutdown 的ScheduledExecutorService、监听器注册后没反注册、logback 的LoggerContext被 static 引用; - 临时缓解:在
conf/context.xml加<context antiresourcelocking="true" reloadable="false"></context>,禁用自动热部署,强制走完整 stop/start 流程。
真正棘手的从来不是参数调多大,而是泄漏对象是否还被某个 static 引用、ThreadLocal 或 JNI 资源死死拽着 —— 这些不会因为增大堆内存而消失,只会推迟爆发时间。











