Chris's Blog

Keep Walking......

Spring NamedParameterJdbcTemplate的内存泄漏问题

我们的项目是使用Spring JDBC来操作DB的,通常是直接使用SimpleJdbcTemplate,一直以来也没发现什么问题。今天在做performance test的时候,发现内存增长很快,甚至出现了out of memory。把heap dump拉下来查看后,发现是这次新加的一个service出现了问题,但根源是因为NamedParameterJdbcTemplate中的HashMap导致的。

这里要先说明使用的Spring的版本是2.5.6,在3.0.3中已修复该问题。

查看调用的trace是service->SimpleJdbcTemplate->NamedParameterJdbcTemplate。一直没有仔细看过SimpleJdbcTemplate的源码,其实它基本都是通过调用NamedParameterJdbcTemplate来完成操作的。SimpleJdbcTemplate的初始化,其实就是初始化NamedParameterJdbcTemplate。

SimpleJdbcTemplate.java
1
2
3
4
5
6
7
8
9
10
11
   public SimpleJdbcTemplate(DataSource dataSource) {
      this.namedParameterJdbcOperations = new NamedParameterJdbcTemplate(dataSource);
   }

   public SimpleJdbcTemplate(JdbcOperations classicJdbcTemplate) {
      this.namedParameterJdbcOperations = new NamedParameterJdbcTemplate(classicJdbcTemplate);
   }

   public SimpleJdbcTemplate(NamedParameterJdbcOperations namedParameterJdbcTemplate) {
      this.namedParameterJdbcOperations = namedParameterJdbcTemplate;
   }

NamedParameterJdbcTemplate的作用是方便使用命名式的参数,以代替使用SQL中的‘?’。NamedParameterJdbcTemplate会将每次解析过后的SQL放在一个HashMap中,以起到cache的作用,而问题就出现这个Map上。

NamedParameterJdbcTemplate.java
1
2
3
4
5
6
7
8
9
10
11
12
   private final Map parsedSqlCache = new HashMap();

   protected ParsedSql getParsedSql(String sql) {
      synchronized (this.parsedSqlCache) {
          ParsedSql parsedSql = (ParsedSql) this.parsedSqlCache.get(sql);
          if (parsedSql == null) {
              parsedSql = NamedParameterUtils.parseSqlStatement(sql);
              this.parsedSqlCache.put(sql, parsedSql);
          }
          return parsedSql;
      }
  }

从上面的代码可以看到,NamedParameterJdbcTemplate不断的将解析过的SQL放入parsedSqlCache中,但并没有任何限制,而我们的service会根据不同的条件产生不同的SQL(条件参数比较多),同时这个SimpleJdbcTemplate也是share的,因此出现了out of memory的问题,当然,测试server的配置比较差也是其中一个原因。

这个问题可以参见Spring Bug 7237,该问题已在3.0.3中修复。

由于该阶段已不可能升级Spring的版本,潜在的风险太大,因此可自己实现一个NamedParameterJdbcTemplate,去override掉getParsedSql方法,可取消cache的作用,或参考Spring 3中的实现。

NamedParameterJdbcTemplate.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
   public static final int DEFAULT_CACHE_LIMIT = 256;

   private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;

   private final Map<String, ParsedSql> parsedSqlCache =
          new LinkedHashMap<String, ParsedSql>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry<String, ParsedSql> eldest) {
          return size() > getCacheLimit();
    }
    };

   public void setCacheLimit(int cacheLimit) {
      this.cacheLimit = cacheLimit;
   }

   public int getCacheLimit() {
      return this.cacheLimit;
   }
  
   protected ParsedSql getParsedSql(String sql) {
      if (getCacheLimit() <= 0) {
          return NamedParameterUtils.parseSqlStatement(sql);
      }
      synchronized (this.parsedSqlCache) {
          ParsedSql parsedSql = this.parsedSqlCache.get(sql);
          if (parsedSql == null) {
              parsedSql = NamedParameterUtils.parseSqlStatement(sql);
              this.parsedSqlCache.put(sql, parsedSql);
          }
          return parsedSql;
      }
   }

然后更改一下SimpleJdbcTemplate的实例化方式,先实例化一个改写过的NamedParameterJdbcTemplate,然后作为SimpleJdbcTemplate构造函数的参数来进行实例化。

Comments