Skip to content

Conversation

@staticlibs
Copy link
Collaborator

This is a backport of the PR #533 to v1.5-variegata stable branch.

Problem

In highly parallel environments, creating DuckDBResultSet and DuckDBPreparedStatement objects causes severe mutex contention on the global java.lang.ref.Finalizer lock. Profiling with Pyroscope showed that ~48% of mutex contentions originated from:

DuckDBResultSet.
→ Object.
→ Finalizer.register
→ Finalizer. [GLOBAL LOCK]

When a class overrides finalize(), the JVM registers every new instance with the Finalizer system using a global lock. In applications executing many concurrent queries (e.g., using ZIO, Akka, or thread pools), this single lock becomes a severe bottleneck, significantly degrading throughput.

Background

The finalize() methods were added in April 2020 (commit 934af9f) as a safety net to release native JNI resources if users forgot to call close(). However, finalize() was deprecated in Java 9 (2017) due to:

  • Unpredictable execution timing (GC-dependent)
  • Performance overhead (extra GC cycles for weak reachability)
  • Global lock contention in the Finalizer registration
  • Single-threaded Finalizer thread becoming a bottleneck

The modern replacement (java.lang.ref.Cleaner) was introduced in Java 9, but this driver targets Java 8 compatibility.

Solution

Remove finalize() from all four classes that had it:

  • DuckDBConnection
  • DuckDBPreparedStatement
  • DuckDBResultSet
  • DuckDBSingleValueAppender

All these classes already implement AutoCloseable with proper close() methods. Users should use try-with-resources:

try (Connection conn = DriverManager.getConnection("jdbc:duckdb:");
PreparedStatement stmt = conn.prepareStatement(sql);
ResultSet rs = stmt.executeQuery()) {
// ...
}

This is standard JDBC best practice and ensures deterministic resource cleanup without relying on GC finalization.

Impact

  • Eliminates Finalizer lock contention entirely
  • Improves throughput in high-concurrency scenarios
  • No behavior change for users who properly close resources
  • Users who relied on finalize() for cleanup will now leak resources if they don't call close() (but finalize() was never guaranteed to run anyway)

This is a backport of the PR duckdb#533 to `v1.5-variegata` stable branch.

Problem
-------
In highly parallel environments, creating DuckDBResultSet and
DuckDBPreparedStatement objects causes severe mutex contention on
the global java.lang.ref.Finalizer lock. Profiling with Pyroscope
showed that ~48% of mutex contentions originated from:

  DuckDBResultSet.<init>
    → Object.<init>
      → Finalizer.register
        → Finalizer.<init>  [GLOBAL LOCK]

When a class overrides finalize(), the JVM registers every new
instance with the Finalizer system using a global lock. In
applications executing many concurrent queries (e.g., using ZIO,
Akka, or thread pools), this single lock becomes a severe
bottleneck, significantly degrading throughput.

Background
----------
The finalize() methods were added in April 2020 (commit 934af9f)
as a safety net to release native JNI resources if users forgot
to call close(). However, finalize() was deprecated in Java 9
(2017) due to:

- Unpredictable execution timing (GC-dependent)
- Performance overhead (extra GC cycles for weak reachability)
- Global lock contention in the Finalizer registration
- Single-threaded Finalizer thread becoming a bottleneck

The modern replacement (java.lang.ref.Cleaner) was introduced in
Java 9, but this driver targets Java 8 compatibility.

Solution
--------
Remove finalize() from all four classes that had it:

- DuckDBConnection
- DuckDBPreparedStatement
- DuckDBResultSet
- DuckDBSingleValueAppender

All these classes already implement AutoCloseable with proper
close() methods. Users should use try-with-resources:

  try (Connection conn = DriverManager.getConnection("jdbc:duckdb:");
       PreparedStatement stmt = conn.prepareStatement(sql);
       ResultSet rs = stmt.executeQuery()) {
      // ...
  }

This is standard JDBC best practice and ensures deterministic
resource cleanup without relying on GC finalization.

Impact
------
- Eliminates Finalizer lock contention entirely
- Improves throughput in high-concurrency scenarios
- No behavior change for users who properly close resources
- Users who relied on finalize() for cleanup will now leak
  resources if they don't call close() (but finalize() was
  never guaranteed to run anyway)
@staticlibs staticlibs merged commit c3abd7c into duckdb:v1.5-variegata Jan 22, 2026
12 checks passed
@staticlibs staticlibs deleted the remove_finalizer_15 branch January 22, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants