I am trying to do some linear algebra in Kotlin/JVM and I have two ways of doing it:
- Using Apache commons-math, which implements matrix multiplication purely on the JVM
- Using OpenCV's
Matclass 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:
- 100.802.138 objects were pending for finalization
- When sorting by object size in bytes, 58% of the heap was taken up by
java.lang.ref.Finalizerand 22% byorg.opencv.core.Mat. - When sorting by instance count, both,
FinalizerandMattake 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:
- When connecting VisualVM to the JVM directly, pressing the button "Perform GC" instantly cleans up the heap (RAM usage drops instantly by multiple GB)
- Calling
System.gc()in my code seems to help sometimes, but not always (I know about all the caveats of callingSystem.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
Matobjects 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.
AutoCloseablebut 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.