From ebd1ceb25700e8e31f8c4731c70c2fc3c2cb3770 Mon Sep 17 00:00:00 2001 From: Jules Ivanic Date: Thu, 22 Jan 2026 16:26:02 +1100 Subject: [PATCH] Remove finalize() methods (1.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 934af9f2) 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) --- src/main/java/org/duckdb/DuckDBConnection.java | 6 ------ src/main/java/org/duckdb/DuckDBPreparedStatement.java | 6 ------ src/main/java/org/duckdb/DuckDBResultSet.java | 6 ------ src/main/java/org/duckdb/DuckDBSingleValueAppender.java | 5 ----- 4 files changed, 23 deletions(-) diff --git a/src/main/java/org/duckdb/DuckDBConnection.java b/src/main/java/org/duckdb/DuckDBConnection.java index 58dd2b84a..238f8f9f3 100644 --- a/src/main/java/org/duckdb/DuckDBConnection.java +++ b/src/main/java/org/duckdb/DuckDBConnection.java @@ -122,12 +122,6 @@ public void rollback() throws SQLException { } } - @Override - @SuppressWarnings("deprecation") - protected void finalize() throws Throwable { - close(); - } - public void close() throws SQLException { if (isClosed()) { return; diff --git a/src/main/java/org/duckdb/DuckDBPreparedStatement.java b/src/main/java/org/duckdb/DuckDBPreparedStatement.java index 10005951a..dc5e45330 100644 --- a/src/main/java/org/duckdb/DuckDBPreparedStatement.java +++ b/src/main/java/org/duckdb/DuckDBPreparedStatement.java @@ -411,12 +411,6 @@ public boolean isClosed() throws SQLException { return conn == null || conn.connRef == null; } - @Override - @SuppressWarnings("deprecation") - protected void finalize() throws Throwable { - close(); - } - @Override public int getMaxFieldSize() throws SQLException { checkOpen(); diff --git a/src/main/java/org/duckdb/DuckDBResultSet.java b/src/main/java/org/duckdb/DuckDBResultSet.java index 6df384326..e4a5c849f 100644 --- a/src/main/java/org/duckdb/DuckDBResultSet.java +++ b/src/main/java/org/duckdb/DuckDBResultSet.java @@ -110,12 +110,6 @@ public void close() throws SQLException { } } - @Override - @SuppressWarnings("deprecation") - protected void finalize() throws Throwable { - close(); - } - public boolean isClosed() throws SQLException { return resultRef == null; } diff --git a/src/main/java/org/duckdb/DuckDBSingleValueAppender.java b/src/main/java/org/duckdb/DuckDBSingleValueAppender.java index 4cbf64b20..95b3b258d 100644 --- a/src/main/java/org/duckdb/DuckDBSingleValueAppender.java +++ b/src/main/java/org/duckdb/DuckDBSingleValueAppender.java @@ -93,11 +93,6 @@ public void append(byte[] value) throws SQLException { } } - @SuppressWarnings("deprecation") - protected void finalize() throws Throwable { - close(); - } - public synchronized void close() throws SQLException { if (appender_ref != null) { DuckDBNative.duckdb_jdbc_appender_close(appender_ref);