Skip to content

Memory leak: Statement.getResultSet() result never freed when statement is closed #780

Description

@zhming0

Version: duckdb_jdbc 1.5.5.0 (also present in this repo's current source). Tested on linux/aarch64, Temurin 25.

Summary

DuckDBPreparedStatement.getResultSet() nulls its own selectResult field before returning:

// getResultSet can only be called once per result
ResultSet to_return = selectResult;
this.selectResult = null;
return to_return;

(https://github.com/duckdb/duckdb-java/blob/main/src/main/java/org/duckdb/DuckDBPreparedStatement.java#L605-L618)

close() calls clearResults() (https://github.com/duckdb/duckdb-java/blob/main/src/main/java/org/duckdb/DuckDBPreparedStatement.java#L1373-L1382), which only frees selectResult when it's non-null. So once getResultSet() detaches the result, it can only ever be freed if the caller explicitly closes the ResultSet.

That means e.g. this standard JDBC 4.3 compliant code leaks the native result per query:

Connection c = DriverManager.getConnection("jdbc:duckdb:");
PreparedStatement s = c.prepareStatement("SELECT 1");
for (int i = 0; i < 100_000; i++) {
    s.execute();
    ResultSet rs = s.getResultSet();   // ownership detached from s
    // rs never closed — closing a Statement must close its ResultSets (JDBC 4.3)
}
s.close();
// each iteration orphans a MaterializedQueryResult; native memory grows linearly

executeQuery() is unaffected — it returns selectResult without nulling it (https://github.com/duckdb/duckdb-java/blob/main/src/main/java/org/duckdb/DuckDBPreparedStatement.java#L313-L320) — which is also how we know the nulling itself is the trigger.

Impact

Any JDBC wrapper that uses execute() + getResultSet() and relies on statement close to clean up results leaks on every query — next.jdbc and Spring's JdbcTemplate both do this. In our production service this retained a MaterializedQueryResult per query, growing native memory (invisible to the JVM heap and to duckdb_memory()) until OOM.

Note

I spent many many hours trying to figure out a memory leak issue for my OSS tool: https://github.com/o11ylite/o11ylite.
I initially thought it was upstream DuckDB issue, which I did discover one/two leaks and @dentiny helped fixing them but it turned out leak still happens after v1.5.5. And that's when I start suspecting my Clojure/Java harness is the culprit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions