最近在写一个对数据库进行批处理并调用其他开源库索引的程序。但总是运行几分钟后就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。可以看出来,正常的内存基本上是从头到尾保持在一个中间值附近震荡,而垃圾回收也绝不会太高。
简化后的代码如下:
- for (int i = 0; i < totalCount; i++) {
- String query = "select ... limit " + i + "," + PER_COUNT;
- pstmt = conn.prepareStatement(query);
- rslt = pstmt.executeQuery();
- while (rslt.next()) {
- //consume rslt...
- }
- }
好吧,问题是找到了,为什么需要在最后关闭ResultSet,难道不是ResultSet引用指向就自动被释放掉了吗?带着这个问题,我查了Mysql connector的实现和JDBC规范。
我们首先来看看Connection的close究竟做了什么事情?
1. log关于该connection的报告
2. 关闭所有在其上已经打开的statement
3. 关闭打开的I/O流
那么Connection是如何关闭所有打开的statement的呢,它从哪里可以得到所有已经打开的Connection呢?请看
- private void closeAllOpenStatements() throws SQLException {
- SQLException postponedException = null;
- if (this.openStatements != null) {
- List<Statement> currentlyOpenStatements = new ArrayList<Statement>(); // we need this to
- // avoid
- // ConcurrentModificationEx
- for (Iterator<Statement> iter = this.openStatements.keySet().iterator(); iter.hasNext();) {
- currentlyOpenStatements.add(iter.next());
- }
- int numStmts = currentlyOpenStatements.size();
- for (int i = 0; i < numStmts; i++) {
- StatementImpl stmt = (StatementImpl) currentlyOpenStatements.get(i);
- try {
- stmt.realClose(false, true);
- } catch (SQLException sqlEx) {
- postponedException = sqlEx; // throw it later, cleanup all
- // statements first
- }
- }
- if (postponedException != null) {
- throw postponedException;
- }
- }
- }
- protected void closeAllOpenResults() throws SQLException {
- synchronized (checkClosed().getConnectionMutex()) {
- if (this.openResults != null) {
- for (ResultSetInternalMethods element : this.openResults) {
- try {
- element.realClose(false);
- } catch (SQLException sqlEx) {
- AssertionFailedException.shouldNotHappen(sqlEx);
- }
- }
- this.openResults.clear();
- }
- }
- }
再来看看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- JDBC Home Page
- Answers to Frequently-Asked Questions
- Using Blob objects (Binary Large Objects) Chapter 8 of the JDBC Java Series book
- Using Clob objects (Character Large Objects) Chapter 10 of the JDBC Java Series book
- Java Series book JDBCTM API Tutorial and Reference, Third Edition
- J2EE documentation web page