由OutOfMemoryError引发的总结与思考

最近在开发一款向数据库中添加Mock数据的命令行工具时,出现了 java.lang.OutOfMemoryError这种问题,工具大概在插入150万条数据总会出现这个问题。本篇博客就记录了如何解决这一问题的过程:

为了进一步学习MySQL数据库的相关知识,需要操作存放大量数据的数据表;为此,出现了开发一款向数据库中添加Mock数据的命令行工具这一日常需求。实现的目标:只需在相关文件中进行配置,便可进行插入数据。

开发的关键步骤是获取数据表的字段名称、字段类型、相关的约束(比如varchar的长度等);下面是获取相关信息的关键代码:

1
2
3
4
5
6
7
PreparedStatement pst = conn.prepareStatement(sql);
ResultSetMetaData rsmd = pst.getMetaData();
for (int i = 0; i < rsmd.getColumnCount(); i++) {
String colName = rsmd.getColumnName(i + 1); // 字段名
String colType = rsmd.getColumnTypeName(i + 1); // 字段类型
Integer precision = rsmd.getPrecision(i + 1); // 字段的约束
}

获取到数据表的字段信息后,接下来就是根据字段的类型、约束来生成相应的数据;然后就是不断地往数据表中插入数据。

但是,程序总会在插入到150万条左右数据的时候出现 java.lang.OutOfMemoryError的问题,如下图:

出现问题的初始代码如下:(测试时insertNum为1000万,perCount为100)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PreparedStatement pst;
for (int i = 0; i < insertNum / perCount; i++) {
for (int j = 0; j < perCount; j++) {
valueStr[j] = filteredColumns.stream().map(column -> "'" + column.randomValue() + "'").collect(Collectors.joining(","));
}
String fieldValues = String.join("),(", valueStr);
String sql = "INSERT INTO " + MySQL.table.getValue() + "(" + fieldStr + ") VALUES (" + fieldValues + ");";
try {
pst = conn.prepareStatement(sql);
pst.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}

刚出现这问题的时候,直接想到可能是因为代码中的字符串太多,导致常量池中的字符串对象数据过多而导致的OOM问题,于是乎将代码中的String都使用StringBuilder来替换。这一方法还是无法避免OOM的出现,似乎对内存的利用没有丝毫帮助。

接下来该如何来解决问题呢?我想肯定不能完全依靠自己的直觉,而是要按照规范的流程来度量问题产生的原因。下面是进行的一些步骤:

  • jps:获取应用的进程号;
  • jinfo -flags pid:获取应用运行时设置的JVM参数,比如-XX:MaxHeapSize等;
  • jvisualvm:启动Java VisualVM工具;

获取到MaxHeapSize后,如果认为其较小,可以将其调大。但是我这个问题无法通过调整MaxHeapSize来解决,因为通过VisualVM工具分析,应用消耗的内存一直持续增大,如果不修改代码,根本无法完成最后的目标。下面是内存的消耗情况:

通过Java VisualVM上的Profiler标签的内存按钮来展示性能分析结果,发现char[]占用的内存极其多,而String类底层实现的原理就是char[]

接下来通过监视标签的堆Dump按钮来产生.hprof文件,可以使用MAT软件来打开该文件,进一步进行分析。对于我这个问题产生的.hprof文件,MAT分析的结果如下:

从上面的图片可以很清楚地看出,JDBC4Connection这个实例占用了极大的内存,从而很快定位到问题的地方:pst = conn.prepareStatement(sql),该方法在循环体中调用,插入140万数据时,该方法调用了1.4万次。结合MAT分析的结果,每次调用pst = conn.prepareStatement(sql)时,conn都会将sql字符串进行保存,即保存了1万多个sql字符串,可想而知占用了多大的内存。

当然这个问题可以通过两种方法解决(具体代码见):

  • 不使用PreparedStatement预编译的方式;
  • 正确地使用PreparedStatement预编译的方式