文章目录
- [一次生产环境 Tomcat 7 + JDK 7 应用启动失败的完整排查与修复实录](#一次生产环境 Tomcat 7 + JDK 7 应用启动失败的完整排查与修复实录)
-
- 一、问题初现:看似成功,实则崩溃
- 二、第一步:解决端口冲突(表面问题)
-
- [🔍 定位占用进程](#🔍 定位占用进程)
- [🛠️ 强制清理](#🛠️ 强制清理)
- [✅ 验证重启](#✅ 验证重启)
- 三、第二步:修复内存泄漏(根本问题)
-
- [1. JDBC 驱动未注销](#1. JDBC 驱动未注销)
- [2. MySQL 后台线程未停](#2. MySQL 后台线程未停)
- [3. Quartz 工作线程泄漏(最严重)](#3. Quartz 工作线程泄漏(最严重))
- [🛠️ 解决方案:添加全局销毁监听器](#🛠️ 解决方案:添加全局销毁监听器)
- [四、第三步:修复 Ehcache RMI 集群配置](#四、第三步:修复 Ehcache RMI 集群配置)
-
- [✅ 正确做法](#✅ 正确做法)
- [🔒 开放防火墙](#🔒 开放防火墙)
- 五、其他优化项
-
- [1. 解决 SLF4J 多绑定冲突](#1. 解决 SLF4J 多绑定冲突)
- [2. 移除 WEB-INF/lib 中的 servlet-api.jar](#2. 移除 WEB-INF/lib 中的 servlet-api.jar)
- 六、最终验证
- 七、经验总结
一次生产环境 Tomcat 7 + JDK 7 应用启动失败的完整排查与修复实录
环境 :CentOS 7 + Tomcat 7.0.96 + JDK 1.7.0_80 + Ehcache 2.6 + Memcached
应用 :Spring MVC 项目/dhmall,含 Quartz 定时任务、Druid 数据源、RabbitMQ、银联支付 SDK
问题现象 :Tomcat 启动后立即崩溃,报Address already in use,伴随大量内存泄漏警告
本文将完整复盘从日志分析、端口冲突解决到资源泄漏修复的全过程,为同类老旧 Java 系统提供排错参考。
一、问题初现:看似成功,实则崩溃
首次查看日志时,被这行迷惑:
log
INFO: Server startup in 23868 ms
但紧接着出现致命错误:
log
SEVERE: StandardServer.await: create[localhost:8088]:
java.net.BindException: Address already in use
随后 Tomcat 自动进入 shutdown 流程,并暴露出一系列严重警告:
log
SEVERE: The web application [] registered the JDBC driver [...] but failed to unregister it
SEVERE: The web application [] appears to have started a thread named [schdulerFactory_Worker-1]...
结论 :这不是简单的"端口占用",而是 残留进程 + 资源泄漏 的复合型故障。
二、第一步:解决端口冲突(表面问题)
🔍 定位占用进程
bash
# 查找占用 8088 或 8005 端口的进程
lsof -i :8088
lsof -i :8005
# 输出示例
java 12345 user ... /apache-tomcat-7.0.96/...
🛠️ 强制清理
bash
kill -9 12345
# 再次检查确保无残留
netstat -tulnp | grep -E '8088|8005'
💡 经验:在频繁部署或异常断电后,Tomcat 进程常"假死"残留,务必手动清理。
✅ 验证重启
bash
/data/soft/apache-tomcat-7.0.96/bin/startup.sh
tail -f logs/catalina.out
此时 Tomcat 成功启动,HTTP 服务可访问。但隐患仍在。
三、第二步:修复内存泄漏(根本问题)
尽管应用能运行,但每次重启都会留下"垃圾",长期将导致 Metaspace OOM。日志中三大泄漏点:
1. JDBC 驱动未注销
log
SEVERE: ... registered the JDBC driver [com.alibaba.druid.proxy.DruidDriver] but failed to unregister
2. MySQL 后台线程未停
log
SEVERE: ... thread named [MySQL Statement Cancellation Timer] ...
3. Quartz 工作线程泄漏(最严重)
log
SEVERE: ... thread named [schdulerFactory_Worker-1] ... memory leak
🛠️ 解决方案:添加全局销毁监听器
创建 AppCleanupListener.java:
java
package com.dhmall.listener;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import net.sf.ehcache.CacheManager;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
public class AppCleanupListener implements ServletContextListener {
@Override
public void contextDestroyed(ServletContextEvent sce) {
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(sce.getServletContext());
// 1. 关闭 Quartz 调度器
try {
Scheduler scheduler = (Scheduler) ctx.getBean("schdulerFactory");
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown(true); // 等待任务完成
}
} catch (Exception e) {
e.printStackTrace();
}
// 2. 关闭 Ehcache(如有集群,会自动注销 RMI)
try {
CacheManager.getInstance().shutdown();
} catch (Exception e) {
e.printStackTrace();
}
// 3. (可选)显式注销 JDBC 驱动(新版通常自动处理)
// DriverManager.deregisterDriver(...);
}
@Override
public void contextInitialized(ServletContextEvent sce) {
// 初始化逻辑(如设置 RMI hostname)
System.setProperty("java.rmi.server.hostname", "172.26.100.204");
}
}
在 web.xml 中注册:
xml
<listener>
<listener-class>com.dhmall.listener.AppCleanupListener</listener-class>
</listener>
✅ 效果:应用停止时,Quartz 线程池优雅关闭,Ehcache 断开 RMI 连接,JDBC 驱动被清理。
四、第三步:修复 Ehcache RMI 集群配置
原配置存在致命错误:
xml
<cacheManagerPeerListenerFactory
properties="hostName=localhost, port=40001, ..." />
hostName=localhost 导致其他节点无法连接!
✅ 正确做法
- 移除
hostName配置 - 通过 JVM 参数指定对外 IP
在 tomcat/bin/setenv.sh(若无则新建)中添加:
bash
export CATALINA_OPTS="$CATALINA_OPTS -Djava.rmi.server.hostname=172.26.1.2"
ehcache.xml 简化为:
xml
<cacheManagerPeerListenerFactory
class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
properties="port=40001, socketTimeoutMillis=2000" />
🔒 开放防火墙
bash
firewall-cmd --permanent --add-port=40001/tcp
firewall-cmd --reload
五、其他优化项
1. 解决 SLF4J 多绑定冲突
日志中:
log
SLF4J: Class path contains multiple SLF4J bindings.
在 pom.xml 中排除冗余绑定:
xml
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
2. 移除 WEB-INF/lib 中的 servlet-api.jar
log
validateJarFile(.../servlet-api-2.5.jar) - jar not loaded.
该 JAR 应由 Tomcat 提供,应用中不应包含。
六、最终验证
- 正常启动 :无
BindException,Server startup in XXX ms后持续运行 - 无泄漏警告 :重启时不再出现
SEVERE: ... memory leak - 集群同步:多节点环境下缓存更新实时生效
- 外部可访问 :
curl http://服务器IP:8088/dhmall/xxx返回正常
七、经验总结
| 问题类型 | 教训 |
|---|---|
| 端口冲突 | 永远先查 lsof,不要被"startup"迷惑 |
| 线程泄漏 | 所有后台线程(Quartz/Ehcache/Timer)必须显式关闭 |
| RMI 配置 | hostName=localhost 是集群最大陷阱 |
| 老旧技术栈 | JDK 7 + Tomcat 7 已 EOL,建议尽快升级 |
最后忠告:对于仍在使用 JDK 7/Tomcat 7 的系统,请务必建立完善的启停脚本和健康检查机制。技术债终需偿还,但在此之前,我们仍要守护好每一行 legacy code 的稳定运行。