一、Tomcat内存泄漏的典型症状

内存泄漏就像家里的水管漏水一样,刚开始可能察觉不到,但时间一长就会导致整个系统崩溃。在Tomcat中运行的应用如果出现内存泄漏,通常会有以下明显症状:

  1. 应用响应越来越慢,就像老牛拉破车
  2. 频繁出现OutOfMemoryError错误
  3. JVM堆内存使用率持续攀升,像坐了火箭一样
  4. 需要不断重启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)是分析内存泄漏的瑞士军刀。它能帮你:

  1. 找出占用内存最多的对象
  2. 显示对象的引用链
  3. 自动检测潜在的内存泄漏

分析步骤:

  1. 用MAT打开堆转储文件
  2. 查看"Leak Suspects"报告
  3. 分析对象保留路径

三、常见内存泄漏场景及修复方案

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);
        }
    }
}

五、内存泄漏预防的最佳实践

  1. 代码审查:建立严格的内存泄漏检查清单
  2. 自动化测试:使用内存分析工具集成到CI/CD流程
  3. 监控告警:配置JVM内存使用监控
  4. 定期演练:模拟内存泄漏场景进行应急演练

示例监控脚本(技术栈: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内存泄漏问题就像慢性病,早期发现和治疗成本最低。通过本文介绍的方法,你应该能够:

  1. 快速识别内存泄漏症状
  2. 使用专业工具准确定位问题
  3. 修复常见的内存泄漏模式
  4. 建立预防机制避免问题复发

记住,预防胜于治疗。良好的编码习惯和定期的内存健康检查,能让你的Tomcat应用跑得更稳、更久。