如何在巨大的文件写入期间克服OutOfMemoryError

时间:2010-09-16 10:44:11

标签: java jdbc out-of-memory

我正在用java编写完整的数据库提取程序。数据库是Oracle,它是巨大的。有些表有大约2.6亿条记录。程序应该以特定格式为每个表创建一个文件,因此使用Oracle数据泵等不是一个选项。此外,某些公司安全策略不允许编写PL / SQL过程以在此服务器上为此要求创建文件。我必须使用Java和JDBC。

我面临的问题是,由于某些表的文件很大(~30GB),即使使用20GB的Java堆,我几乎每次都会耗尽内存。在文件大小超过堆大小的文件创建期间,即使使用最激进的GC策略之一,该过程似乎也会挂起。例如,如果文件大小> 20GB和堆大小为20GB,一旦堆利用率达到最大堆大小,它每分钟写入速度减慢2MB左右,并且以此速度,需要几个月才能获得完全提取。

我正在寻找一些方法来克服这个问题。任何帮助将不胜感激。

以下是我所拥有的系统配置的一些细节: Java - JDK1.6.0_14

系统配置 - 在4 X Intel Xeon E7450(6核)@ 2.39GH上运行的RH Enterprise Linux(2.6.18)

RAM - 32GB

数据库Oracle 11g

文件的部分代码如下:

private void runQuery(Connection conn, String query, String filePath,
        String fileName) throws SQLException, Exception {
    PreparedStatement stmt = null;
    ResultSet rs = null;
    try {
        stmt = conn.prepareStatement(query,
                ResultSet.TYPE_SCROLL_INSENSITIVE,
                ResultSet.CONCUR_READ_ONLY);
        stmt.setFetchSize(maxRecBeforWrite);
        rs = stmt.executeQuery();
        // Write query result to file
        writeDataToFile(rs, filePath + "/" + fileName, getRecordCount(
                query, conn));
    } catch (SQLException sqle) {
        sqle.printStackTrace();
    } finally {
        try {
            rs.close();
            stmt.close();
        } catch (SQLException ex) {
            throw ex;
        }
    }
}

private void writeDataToFile(ResultSet rs, String tempFile, String cnt)
        throws SQLException, Exception {
    FileOutputStream fileOut = null;
    int maxLength = 0;
    try {
        fileOut = new FileOutputStream(tempFile, true);
        FileChannel fcOut = fileOut.getChannel();

        List<TableMetaData> metaList = getMetaData(rs);
        maxLength = getMaxRecordLength(metaList);
        // Write Header
        writeHeaderRec(fileOut, maxLength);
        while (rs.next()) {
            // Now iterate on metaList and fetch all the column values.
            writeData(rs, metaList, fcOut);
        }
        // Write trailer
        writeTrailerRec(fileOut, cnt, maxLength);
    } catch (FileNotFoundException fnfe) {
        fnfe.printStackTrace();
    } catch (IOException ioe) {
        ioe.printStackTrace();
    } finally {
        try {
            fileOut.close();
        } catch (IOException ioe) {
            fileOut = null;
            throw new Exception(ioe.getMessage());
        }
    }
}

private void writeData(ResultSet rs, List<TableMetaData> metaList,
        FileChannel fcOut) throws SQLException, IOException {
    StringBuilder rec = new StringBuilder();
    String lf = "\n";
    for (TableMetaData tabMeta : metaList) {
        rec.append(getFormattedString(rs, tabMeta));
    }
    rec.append(lf);
    ByteBuffer byteBuf = ByteBuffer.wrap(rec.toString()
            .getBytes("US-ASCII"));
    fcOut.write(byteBuf);
}

private String getFormattedString(ResultSet rs, TableMetaData tabMeta)
        throws SQLException, IOException {
    String colValue = null;
    // check if it is a CLOB column
    if (tabMeta.isCLOB()) {
        // Column is a CLOB, so fetch it and retrieve first clobLimit chars.
        colValue = String.format("%-" + tabMeta.getColumnSize() + "s",
                getCLOBString(rs, tabMeta));
    } else {
        colValue = String.format("%-" + tabMeta.getColumnSize() + "s", rs
                .getString(tabMeta.getColumnName()));
    }
    return colValue;

}

5 个答案:

答案 0 :(得分:3)

可能是由于您拨打prepareStatement的方式,请参阅this question了解类似问题。您不需要可滚动性,ResultSet将是只读的默认设置,因此只需调用

即可
stmt = conn.prepareStatement(query);

答案 1 :(得分:1)

修改: 使用JPA将数据库表映射到Class 现在使用Hibernate以一些可容忍的大小批量加载来自DB的对象集合,并将其序列化为FILE。

答案 2 :(得分:0)

您的算法是否如下所示?这假设DB行和文件中的行之间有直接映射:

// open file for writing with buffered writer.
// execute JDBC statement
// iterate through result set
    // convert rs to file format
    // write to file
// close file
// close statement/rs/connection etc

尝试使用Spring JDBC Template来简化JDBC部分。

答案 3 :(得分:0)

我相信这在默认的32 MB java堆上必须是可能的。只需获取每一行,将数据保存到文件流,闪存并​​关闭一次。

答案 4 :(得分:0)

您对maxRecBeforWrite有什么价值?

也许最大记录长度的查询是通过强制JDBC扫描整个结果的记录长度来破坏你的setFetchSize?也许你可以延迟编写标题并注意动态的最大记录大小。