JDBC的误用-从一个outofmemory中想到的

 最近在写一个对数据库进行批处理并调用其他开源库索引的程序。但总是运行几分钟后就OOM,最初的报错让我怀疑是那个开源库不够健壮,后来才发现是JDBC程序写得有问题。

  OOM的确非常难定位问题,因为很可能耗内存的大户是A,但是有可能不幸的B刚好在分配内存时撞上OOM,于是堆栈都是关于B的错误,如果顺着堆栈,很可能会误导我们。

  遇到OOM不要慌张,可以打开神器jvirtualvm,该神器的无敌强大就不介绍了,在新版的jdk中都自带,当然了,linux命令行下面只能用jmap等等类似的命令行工具。下面且来看看我遇到的OOM,可以看到随着时间上升,堆空间不断的锯齿状上升,而且上升幅度很大,如果同时看看jvm的垃圾回收情况,就会明白垃圾回收越来越剧烈(因为每次都快要触碰内存的最大边界),在中间还可以点击将内存dump出来以便事后分析。

  


  使用抽样器分析dump出来的内存,发现占用内存最多的对象是byte数组,达到82%,这种情况非常的异常,说明某种对象hold住了byte数组不加释放。

  

  连续点进去查看几组byte数组的引用,发现有好几个地方的对象都对其做了引用。但是来源于jdbc的引用要多于其他几个来源。难道jdbc有什么问题?我的确是关闭connection了。中间用了多次的类似分页的处理,在最后完毕时关掉了connection,但是每次处理时并没有关闭ResultSet和PreparedStatement,好吧,试一把每次分页时close掉他们。再次监控时恢复正常,连续运行了一小时后也没有再出现OOM。可以看出来,正常的内存基本上是从头到尾保持在一个中间值附近震荡,而垃圾回收也绝不会太高。



  简化后的代码如下:

[java]  view plain copy
  1. for (int i = 0; i < totalCount; i++) {  
  2.     String query = "select ... limit " + i + "," + PER_COUNT;     
  3.     pstmt = conn.prepareStatement(query);  
  4.     rslt = pstmt.executeQuery();  
  5.   
  6.     while (rslt.next()) {  
  7.         //consume rslt...  
  8.     }     
  9. }  

  好吧,问题是找到了,为什么需要在最后关闭ResultSet,难道不是ResultSet引用指向就自动被释放掉了吗?带着这个问题,我查了Mysql connector的实现和JDBC规范。

  我们首先来看看Connection的close究竟做了什么事情

  1. log关于该connection的报告

  2. 关闭所有在其上已经打开的statement

  3. 关闭打开的I/O流

  那么Connection是如何关闭所有打开的statement的呢,它从哪里可以得到所有已经打开的Connection呢?请看

[java]  view plain copy
  1. private void closeAllOpenStatements() throws SQLException {  
  2.         SQLException postponedException = null;  
  3.   
  4.         if (this.openStatements != null) {  
  5.             List<Statement> currentlyOpenStatements = new ArrayList<Statement>(); // we need this to  
  6.             // avoid  
  7.             // ConcurrentModificationEx  
  8.   
  9.             for (Iterator<Statement> iter = this.openStatements.keySet().iterator(); iter.hasNext();) {  
  10.                 currentlyOpenStatements.add(iter.next());  
  11.             }  
  12.   
  13.             int numStmts = currentlyOpenStatements.size();  
  14.   
  15.             for (int i = 0; i < numStmts; i++) {  
  16.                 StatementImpl stmt = (StatementImpl) currentlyOpenStatements.get(i);  
  17.   
  18.                 try {  
  19.                     stmt.realClose(falsetrue);  
  20.                 } catch (SQLException sqlEx) {  
  21.                     postponedException = sqlEx; // throw it later, cleanup all  
  22.                     // statements first  
  23.                 }  
  24.             }  
  25.   
  26.             if (postponedException != null) {  
  27.                 throw postponedException;  
  28.             }  
  29.         }  
  30.     }  
  很明显,Connection自身持有一个openStatements,它是一个Map<Statement, Statement> ,到这里就可以想通了,每次我的循环创建Statement但不关闭它,只要Connection没有关闭就会一直持有打开的Statement的引用。到了这里,如果叫我猜测的话,我觉得很可能就像一个Connection有多个openStatement的引用一样,一个Statement也有多个ResultSet的引用,同样是关闭了statement就会关闭resultset。空猜无凭,看看statement中的代码:

[java]  view plain copy
  1. protected void closeAllOpenResults() throws SQLException {  
  2.         synchronized (checkClosed().getConnectionMutex()) {  
  3.             if (this.openResults != null) {  
  4.                 for (ResultSetInternalMethods element : this.openResults) {  
  5.                     try {  
  6.                         element.realClose(false);  
  7.                     } catch (SQLException sqlEx) {  
  8.                         AssertionFailedException.shouldNotHappen(sqlEx);  
  9.                     }  
  10.                 }  
  11.       
  12.                 this.openResults.clear();  
  13.             }  
  14.         }  
  15.     }  
  好吧,这次openResults不是Map,而是Set,但是,没有区别,同样被上层statement引用了,如果不释放,就会越积越多。

  再来看看Statement的close做了什么事

  实际上,观察代码会发现基本上它会做两件事

  1. 负责释放所有已经打开的resultset。

  2. 从上层Connection的引用集合中脱离。

  ResultSet呢

  1. log关于该ResultSet的报告

  2. 同样从上层的Statement的引用集合中脱离。

  3. 关闭RowData

  结论:Connection持有多个statement,每个statement持有多个ResultSet,当需要复用connection时,为了避免他们hold住内存,应当在适当的时候分别释放他们

  参考文档

  Mysql Java驱动代码阅读笔记及JDBC规范笔记 http://blog.csdn.net/hengyunabc/article/details/7722147

  jdbc规范

  Specification documents for all releases of the JDBC API are available from the JDBC download page.

  Tutorials and Programming Guides
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值