javakotlinopencvmemory-leaksgarbage-collection

Native objects do not trigger garbage collection


I am trying to do some linear algebra in Kotlin/JVM and I have two ways of doing it:

  1. Using Apache commons-math, which implements matrix multiplication purely on the JVM
  2. Using OpenCV's Mat class and linear algebra functions

The advantage of doing it with OpenCV is of course speeeeeed, but the disadvantage is, that Mat objects are in native memory, not in JVM memory. Apache-commons on the other hand implements linear algebra purely on the JVM, which is slow, but at least memory is managed properly.

To get around the memory issue of OpenCV, I wrapped the Mat in a wrapper class. That wrapper class registers a Cleaner which in turn calls Mat.release(), thus freeing up the memory:

import nu.pattern.OpenCV
import org.opencv.core.Core
import org.opencv.core.CvType
import org.opencv.core.Mat
import java.lang.ref.Cleaner

val cleaner: Cleaner by lazy {
    Cleaner.create()
}

fun main() {
    OpenCV.loadLocally()
    val initial = OpenCVMatrixDataContainer(Mat.eye(3, 3, CvType.CV_64F))
    var running = OpenCVMatrixDataContainer(Mat.eye(3, 3, CvType.CV_64F))
    repeat(10000) {
        // Old running-objects are constantly going out-of-scope, yet the cleaner is never called. 
        // Instead, the JVM just quits.
        val oldRunning = running
        running += initial
        // only when uncommenting this is the cleaner actually called. 
        // oldRunning.close()
    }
}

class OpenCVMatrixDataContainer(
    private val mat: Mat,
) : AutoCloseable {
    private val cleanable: Cleaner.Cleanable = cleaner.register(this, CleanRunnable(mat))

    operator fun plus(other: OpenCVMatrixDataContainer): OpenCVMatrixDataContainer {
        val result = Mat()
        Core.add(this.mat, other.mat, result)
        return OpenCVMatrixDataContainer(result)
    }

    // This class has more members to perform linear algebra operations which I left out here for brevity

    override fun close() {
        cleanable.clean()
    }

    private class CleanRunnable(
        val mat: Mat,
    ) : Runnable {
        override fun run() {
            println("Cleaner called #$cleanerCallCount")
            cleanerCallCount++
            mat.release()
        }
    }
}

// For demonstration only
private var cleanerCallCount = 0

When running my application, I realized, that RAM usage went through the roof when doing linear algebra with OpenCV. I therefore switched back to apache commons-math, which is much slower, but RAM usage was reasonable. I therefore started investigating if my OpenCVMatrixDataContainer class is leaking memory. I took a heap dump and examined the heapdump in VisualVM and found the following:

  1. 100.802.138 objects were pending for finalization
  2. When sorting by object size in bytes, 58% of the heap was taken up by java.lang.ref.Finalizer and 22% by org.opencv.core.Mat.
  3. When sorting by instance count, both, Finalizer and Mat take up 40% each.

I am still in the process of investigating this, but most of those Mat-instances seem to be dead, but not yet garbage collected.

Additionally, I found the following things:

  1. When connecting VisualVM to the JVM directly, pressing the button "Perform GC" instantly cleans up the heap (RAM usage drops instantly by multiple GB)
  2. Calling System.gc() in my code seems to help sometimes, but not always (I know about all the caveats of calling System.gc(), no need to educate me about that in the comments).

My conclusions are therefore as follows:

Because this does not seem to be a memory leak in the classical sense, I am out of ideas on how I could proceed.

For context, here is more information about my JVM:

root@dcdee140fe0e:/home# java -version
Picked up JAVA_TOOL_OPTIONS: -Xmx120G
openjdk version "21.0.8" 2025-07-15
OpenJDK Runtime Environment (build 21.0.8+9-Ubuntu-0ubuntu122.04.1)
OpenJDK 64-Bit Server VM (build 21.0.8+9-Ubuntu-0ubuntu122.04.1, mixed mode, sharing)

The application is launched with the following command line:

java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -classpath "server.jar:lib/*:libProject/*:." server.MainKt

Edit: I investigated the heapdump some more and found that there are at least 1000 Mat instances that are only referenced by their CleanRunnable and Finalizer and are thus eligible for GC.


Solution

  • You already nailed it: “I am guessing that GC only sees this tiny memory usage and does not see the need to run itself

    The garbage collector won’t be triggered by resource exhaustion other than heap memory.

    But you also wrote:

    Also, I am aware that OpenCV overrides the finalize method to do the clean-up if the user forgot to call release().

    This implies that your Cleaner won’t improve the situation. Both, Cleaner and finalize() are meant as a best-effort cleanup in case the the user forgot the explicit cleanup. They have the same flaws, for example, of possibly never running.

    Explicit cleanup, i.e. calling close() or using try-with-resource, is the preferable method and if you have the problem of too many pending cleaners or finalizers, you should address the missing explicit cleanup in your application.

    See also Should Java 9 Cleaner be preferred to finalization?