一、Tomcat内存泄漏的典型症状
内存泄漏就像家里的水管漏水一样,刚开始可能察觉不到,但时间一长就会导致整个系统崩溃。在Tomcat中运行的应用如果出现内存泄漏,通常会有以下明显症状:
- 应用响应越来越慢,就像老牛拉破车
- 频繁出现OutOfMemoryError错误
- JVM堆内存使用率持续攀升,像坐了火箭一样
- 需要不断重启Tomcat才能维持正常运行
举个例子,我们来看一个典型的Java Web应用内存泄漏场景(技术栈:Java + Tomcat 9.x):
public class LeakyServlet extends HttpServlet {
// 这个静态Map会不断增长,导致内存泄漏
private static Map<String, Object> cache = new HashMap<>();
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 每次请求都把用户信息存入静态Map
String userId = req.getParameter("userId");
User user = getUserFromDB(userId); // 从数据库获取用户信息
cache.put(userId, user); // 这里就是泄漏点!
// ...其他业务逻辑
}
private User getUserFromDB(String userId) {
// 模拟数据库查询
return new User(userId, "用户"+userId);
}
}
class User {
String id;
String name;
// 构造方法和getter/setter省略...
}
这个例子中,静态Map cache会不断累积用户对象,而且永远不会被垃圾回收。就像往一个无底洞里扔东西,迟早会把内存耗尽。
二、诊断内存泄漏的专业工具
工欲善其事,必先利其器。诊断Tomcat内存泄漏,我们需要借助一些专业工具:
1. JDK自带工具三剑客
- jps:查看Java进程列表
- jmap:生成堆内存快照
- jvisualvm:图形化分析工具
使用示例(技术栈:JDK 8+):
# 1. 首先找到Tomcat进程ID
jps -l
# 2. 生成堆转储文件
jmap -dump:format=b,file=heap.hprof <pid>
# 3. 使用jvisualvm分析
jvisualvm heap.hprof
2. 内存分析神器MAT
Eclipse Memory Analyzer Tool (MAT)是分析内存泄漏的瑞士军刀。它能帮你:
- 找出占用内存最多的对象
- 显示对象的引用链
- 自动检测潜在的内存泄漏
分析步骤:
- 用MAT打开堆转储文件
- 查看"Leak Suspects"报告
- 分析对象保留路径
三、常见内存泄漏场景及修复方案
1. 静态集合滥用
这是最常见的内存泄漏场景,就像我们开头的例子。修复方案:
public class FixedServlet extends HttpServlet {
// 使用WeakHashMap替代普通HashMap
private static Map<String, Object> cache = new WeakHashMap<>();
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
String userId = req.getParameter("userId");
User user = getUserFromDB(userId);
// 现在当内存不足时,缓存会被自动清理
cache.put(userId, user);
// 或者更好的方案:使用专业的缓存框架如Guava Cache
Cache<String, User> betterCache = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
betterCache.put(userId, user);
}
}
2. 未关闭的资源
数据库连接、文件流等资源未关闭也会导致内存泄漏:
public class ResourceLeakServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
Connection conn = null;
try {
conn = dataSource.getConnection(); // 获取数据库连接
// 执行SQL操作...
} catch (SQLException e) {
e.printStackTrace();
}
// 忘记关闭连接!会导致连接池耗尽
}
}
修复方案:使用try-with-resources语法(技术栈:Java 7+)
public class FixedResourceServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 自动关闭资源
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement()) {
// 执行SQL操作...
} catch (SQLException e) {
e.printStackTrace();
}
}
}
3. ThreadLocal使用不当
ThreadLocal是另一个常见的内存泄漏源:
public class ThreadLocalLeak {
private static ThreadLocal<SimpleDateFormat> dateFormatHolder =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public void processRequest() {
SimpleDateFormat sdf = dateFormatHolder.get();
// 使用sdf格式化日期...
}
// 当线程来自线程池时,如果不清理ThreadLocal,它的值会一直存在
}
修复方案:及时清理ThreadLocal
public class FixedThreadLocalUsage {
public void processRequest() {
try {
SimpleDateFormat sdf = dateFormatHolder.get();
// 使用sdf...
} finally {
// 请求处理完成后清理
dateFormatHolder.remove();
}
}
}
四、Tomcat特有的内存泄漏场景
1. 未注销的Servlet和Filter
在Tomcat中,如果你动态注册了Servlet或Filter但没有正确注销,会导致内存泄漏:
public class DynamicServletRegistrar {
public void registerServlet(ServletContext context) {
ServletRegistration.Dynamic registration =
context.addServlet("dynamicServlet", new DynamicServlet());
registration.addMapping("/dynamic/*");
// 如果不保存registration引用,就无法注销这个Servlet
}
}
修复方案:维护Servlet注册引用
public class FixedDynamicRegistration {
private List<ServletRegistration.Dynamic> registrations = new ArrayList<>();
public void registerServlet(ServletContext context) {
ServletRegistration.Dynamic registration =
context.addServlet("dynamicServlet", new DynamicServlet());
registration.addMapping("/dynamic/*");
registrations.add(registration);
}
public void unregisterAll() {
for (ServletRegistration.Dynamic reg : registrations) {
// 注销所有动态注册的Servlet
reg.setAsyncSupported(false); // 先禁用异步支持
reg.getMappings().clear(); // 清除所有映射
}
registrations.clear();
}
}
2. 未清理的Session监听器
如果你的应用向HttpSession添加了监听器但没有移除,也会导致内存泄漏:
public class SessionLeakListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
// 添加一个属性监听器
se.getSession().addAttributeListener(new MyAttributeListener());
}
// 忘记实现sessionDestroyed方法!
}
修复方案:正确实现session监听器
public class FixedSessionListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
MyAttributeListener listener = new MyAttributeListener();
se.getSession().addAttributeListener(listener);
// 将监听器存储在session中以便后续移除
se.getSession().setAttribute("myListener", listener);
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
// 获取并移除监听器
MyAttributeListener listener =
(MyAttributeListener) se.getSession().getAttribute("myListener");
if (listener != null) {
se.getSession().removeAttributeListener(listener);
}
}
}
五、内存泄漏预防的最佳实践
- 代码审查:建立严格的内存泄漏检查清单
- 自动化测试:使用内存分析工具集成到CI/CD流程
- 监控告警:配置JVM内存使用监控
- 定期演练:模拟内存泄漏场景进行应急演练
示例监控脚本(技术栈:Shell + JMX):
#!/bin/bash
# 监控Tomcat内存使用情况
TOMCAT_PID=$(jps -l | grep Bootstrap | cut -d' ' -f1)
MAX_MEM=$(jstat -gccapacity $TOMCAT_PID | awk 'NR==2 {print $4}')
USED_MEM=$(jstat -gc $TOMCAT_PID | awk 'NR==2 {print $3}')
# 计算内存使用率
MEM_USAGE=$((100 * $USED_MEM / $MAX_MEM))
# 如果使用率超过90%,发送告警
if [ $MEM_USAGE -gt 90 ]; then
echo "内存使用率过高: $MEM_USAGE%" | mail -s "Tomcat内存告警" admin@example.com
fi
六、总结与建议
Tomcat内存泄漏问题就像慢性病,早期发现和治疗成本最低。通过本文介绍的方法,你应该能够:
- 快速识别内存泄漏症状
- 使用专业工具准确定位问题
- 修复常见的内存泄漏模式
- 建立预防机制避免问题复发
记住,预防胜于治疗。良好的编码习惯和定期的内存健康检查,能让你的Tomcat应用跑得更稳、更久。
评论