2

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:

  • The objects are not leaking per se, as they are eligible for finalization and forcing GC to run through VisualVM seems to clean those objects up
  • Since Mat objects store the data in native memory, their memory footprint in JVM memory is tiny (it's basically just a pointer). I am guessing that GC only sees this tiny memory usage and does not see the need to run itself.

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

  • Is there any way how GC can be made aware of this larger footprint?
  • Are there any JVM options to run GC more often?

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.

17
  • 3
    Something does not quite add up. java.lang.ref.Finalizer is associated with the use of a finalize method, which you do not show. Commented Sep 12 at 11:23
  • 3
    minimal reproducible example required. do not expect us to take your claims at face value. it's on you to make your issue at least potentially reproducible. -- I really don't see how such a question has earned three upvotes. is that typical for java/kotlin questions? Commented Sep 12 at 11:23
  • 4
    Your class does it right by implementing AutoCloseable but apparently you’re not using the class as intended, as the whole point of the design is to do the cleanup explicitly when done with the object, instead of relying on the garbage collector. Commented Sep 12 at 13:08
  • 5
    Sure, that’s the intended use case, “do implicit clean up if the caller forgets to call close()” but your problem description indicates that there’s a massive dementia regarding the close() call in your application. Forgetting to call close() should be the exception (preferable never happen at all), not the norm. Relying on a cleaner is not better than relying on the finalize() method. Commented Sep 12 at 13:24
  • 5
    And /that/'s why the MRE is required. And why the code you posted isn't an MRE. Commented Sep 12 at 13:32

1 Answer 1

2

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?

Sign up to request clarification or add additional context in comments.

4 Comments

Thank you again for your time and effort. Cheers :)
If a request to allocate a direct buffer fails due to not having enough free space, System.gc will be invoked by the JVM. If that occurs, any Java (direct)ByteBuffers. that are no longer reachable, will be collected, in turn causing a deallocation of the (direct) buffers they point to.
I do not believe this is completely accurate. What is described is memory polluted with content which needs to be processed through the finalizer. Any object which has the finalize method /must/ be processed through the finalizer, even if the correct close method was called. It appears that perhaps the system is cpu starved and the finalization thread is not able to process at the same rate the objects are being created. What would help is if matlab itself removed the finalizer method and moved to use of Cleaner.
Cleaner threads can stave the same way as finalizer threads. Switching from finalizer to cleaner alone doesn’t help. There is no way around using explicit cleanup. Then, if the matlab doesn’t remove the finalizer you can use --finalization=disabled since Java 18.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.