【问题标题】:JVM does not release memory for processed ResultSet objectsJVM 不会为已处理的 ResultSet 对象释放内存
【发布时间】:2021-03-04 05:02:27
【问题描述】:

我需要将从 jdbc ResultSet 提取的约 5000 万行写入 CSV 文件。
写入 CSV 文件的 150 万行大约为 1 GB。

jdbcTemplate.query(new CustomPreparedStatementCreator(arg), new ResultSetExtractor<Void>() {
    @Override
    public Void extractData(ResultSet rs) {
    while (rs.next()) {
        // transform each row's data (involves creation of objects)
        // write the transformed strings to csv file
    }
}

问题是我有一个 8 GB 的堆,它很快就被填满了。
因此,在达到 1000 万行之前,我遇到了 java.lang.OutOfMemoryError。
我的另一个限制是查询读/写超时设置为 30 分钟。

如何回收和重用 JVM 堆内存?
尤其是为我不再需要的对象分配的内存。

read说强制GC运行并不能保证内存会被回收。

我有哪些选择?我是否应该通过 JNA 或 JNI 将责任交给非 GC 语言
如 C、C++ 或 JNI 来处理 ResultSet?

[编辑] 看来我处境艰难:D 添加更多信息,正如@rzwitserloot 指出的那样

  1. 我正在从与数据湖挂钩的数据虚拟化工具中读取(仅限 SELECT 查询)数据。
  2. 数据虚拟化工具的 jdbc 驱动程序确实支持 LIMIT,但查询是由业务设计的,以返回大量数据。所以我可以一次性提取数据并生成 CSV - 这意味着,我无法避免巨大的 SELECT 或放置一个 LIMIT 子句
  3. 我需要检查这些属性:resultSetTyperesultSetConcurrencyresultSetHoldability

我已经做了什么:

首先,我使用生产者-消费者模式将 jdbc 获取操作与慢速文件写入操作分开。这有助于在 30 分钟超时之前创建包含 1-5 百万行的 CSV 文件。

其次,我增加了消费者线程的数量,并让它们写入自己单独的部分文件,以便稍后合并到单个 CSV 文件中。这加快了文件写入速度,并在 30 分钟超时之前创建了一个包含 10-2000 万行的 CSV 文件。

我在 ResultSetExtractor 中创建对象并通过有界队列将其传递给消费者线程。一旦将这些对象中的数据写入文件,就不再需要这些对象。

【问题讨论】:

  • 你考虑过RowCallbackHandler的使用吗? (mkyong.com/spring/spring-jdbctemplate-handle-large-resultset)
  • 我注意到,如果你快速清理堆,GC 可能会太慢而无法释放。 “强制” GC 运行并不能保证它,但 System.gc() 对我在这种情况下避免内存问题是有效的,在这种情况下,我放入循环 if (Runtime.getRuntime().freeMemory() &lt; someThresholdTweakedForPerformance) { System.gc(); }。从长远来看,这可能不是最佳选择,但它确实有效。
  • @DaveH 我不确定这会有所帮助。如果您查看示例,它会在堆上创建一个 String 对象。 String name = resultSet.getString("Name"); 这将保留在那里,如果我们从结果集中获得另一个具有相同值的字符串,它将被引用。
  • @DanielWiddis,我实际上正在快速咀嚼堆,这是真的。这分解为大约 6-8 百万行,因为在同一个 JVM 实例上运行着其他任务。因此,调用System.gc(); 并不能保证回收内存 - 此任务需要保证能够生成 CSV。
  • 字符串真的会留在堆中吗?在循环外部声明变量并在内部重新分配它。 “旧”字符串将立即可用于垃圾收集,因为它没有对它的活动引用。您的问题主要不是 rowsetextractor 是构建一个包含 5000 万行的列表,然后按顺序处理它吗? RowCallBackHandler 将处理从结果集返回的每一行

标签: java jvm java-native-interface jna


【解决方案1】:

您粘贴的代码很少;关键线索之一是设计,您粘贴的代码没有内存问题 - ResultSet 被有意设计为游标式,这意味着理论上每个 @987654321 @call 导致 TCP/IP 流量,要求数据库获取另一行。这就是需要关闭结果集的原因(因为数据库正在维护一个单独的“版本”,因此,假设您使用可序列化或其他一些干净读取隔离级别,任何其他已启动的事务(或者更确切地说,尚未提交) ) 当您打开您所在的*时,不会对您在通过.next() 电话时看到的数据产生任何影响。

现在,JDBC API 也相当灵活。例如,大量的数据包和流量和工作,所以在实践中,许多 DB JDBC 驱动程序要么一次发送所有数据,而 resultset .close 什么都不做,或者至少会发送一些东西在较大的批次中,大多数 .next() 调用不会导致数据库流量,除了每 100 次调用或其他情况。

因此,我们在这里有 2 个主要选项:

  1. 内存泄漏与您粘贴的内容无关;例如,您将 CSV 数据写入一个不断增长的缓冲区,而您根本没有将其流式传输到磁盘。三重检查这个。用 LIMIT 子句替换你的巨大 SELECT 并在它周围添加一个巨大的 for 循环来模拟写入大量记录,而无需从 JDBC 循环中实际查询太多。如果仍然内存不足,则不是您的 db 层。

  2. 但 JDBC 驱动程序仍在使用不断占用内存的东西来实现其 ResultSet 实现。

如果是#2,那么你有2个解决方案:

  1. 让数据库引擎不这样做。结果集具有“功能”,您在制作它们时询问您需要哪些功能。例如,您可以告诉系统您希望结果集是所谓的“仅向前”。最有可能导致非内存咀嚼结果集的 3 个属性是使用 resultSetType = FORWARD_ONLYresultSetConcurrency = CONCUR_READ_ONLYresultSetHoldability = CLOSE_CURSORS_AT_COMMIT 初始化的属性。我实际上不知道如何告诉 jdbctemplate 执行此操作,但这应该不会太难 - jdbctemplate 正在调用 java.sql.ConnectionprepareStatement 方法 - 确保它调用您将所有这些属性设置为那些的那个价值观。

  2. 如果这不起作用,请解决它,使用 OFFSET/LIMIT(不幸的是,此语法取决于您的数据库引擎)一次获取页面。当然,如果在您执行此操作时正在编辑表,除非您设置了可序列化的事务级别,否则这会弄乱您的东西,您必须添加某种 ORDER BY 子句,否则您不会得到实际保证结果以相同的顺序返回(没有它,OFFSET/LIMIT 分页不会做你想要的)。这有点奇怪 - 如果您遇到这种情况,您使用的是哪种奇特的三流写得不好的数据库引擎和/或 JDBC 驱动程序?

*) “但是​​,我没有使用交易!” - 是的,你是,'auto commit = true' 就是通常所说的'无事务',但事实并非如此;您发送的每个 SQL 语句只需 1 个事务。唯一真正的“无事务”模式是您的数据库明确表示存在于事务之外的东西,不是真正安全的数据库,例如具有 MyISAM 表类型的 MySQL,它首先不是真正的数据库,或者故意宽松隔离级别。

【讨论】:

    猜你喜欢
    • 2021-01-11
    • 1970-01-01
    • 2022-01-10
    • 1970-01-01
    • 1970-01-01
    • 2015-05-31
    • 1970-01-01
    • 1970-01-01
    • 2011-12-27
    相关资源
    最近更新 更多