I have a project that contains a class similar to this:
data class Project(val someMemoryHeavyMember: String) {
companion object {
fun readFile(file: File): Project {
TODO("This deserializes the project from disk")
}
}
}
This class contains some memory-heavy members, and I have realized that it seems to leak memory. I therefore started to investigate what referenced it and thus prevented garbage collection. To understand my findings, some more context is necessary. Usually, instances of Project
are loaded asynchronously using a JavaFX Task<Project>
:
fun loadProject(file: File): Task<Project> = object : Task<Project>() {
init {
updateTitle("Loading ${file.name}...")
}
override fun call(): Project {
updateMessage("Reading project file...")
return Project.readFile(file)
}
}.attachModalProgressUi().scheduleAsBackgroundTask()
fun <T> Task<T>.attachModalProgressUi() = apply {
// Creates a org.controlsfx.control.TaskProgressView and submits this task to it.
// Most importantly, it also does the following:
this.appendOnCancelled { removeAndHideIfApplicable() }
.appendOnFailed { removeAndHideIfApplicable() }
.appendOnSucceeded { removeAndHideIfApplicable() }
}
private fun Task<*>.removeAndHideIfApplicable() {
taskView.tasks.remove(this)
if (taskView.tasks.isEmpty()) stage.hide()
}
So, in summary, the asynchronous Task
gets created, scheduled and is submitted to a ControlsFX TaskProgressView
to show the loading progress in the UI. Once the task finishes, it gets removed from the TaskProgressView
again and the progress UI is hidden. This works flawlessly.
Desired behavior: Loading a new project should remove all references to the previously loaded project, causing it to get garbage collected.
Actual behavior: All code-references are removed, however, VisualVM still shows the following references leading to GC-roots:
This screenshot shows, that:
Task
stores its result in a field called outcome
(this is desired behavior)Task
cannot be garbage collected because some text view still references it.ListView
in a field called far
that in turn is referenced through the ControlsFX TaskProgressView
again in a field called far
.However, when browsing the sources for TaskProgressView
and ListView
, neither of them or one of their superclasses contains a field called far
.
What I found so far: Frankly, not a lot. Google has not shown any significant results. ChatGPT said the following:
In OpenJDK, the field "far" in a heap dump does not correspond to a standard class or public API. It may be related to internal optimizations or memory management features used by the JVM.
My questions therefore are:
far
coming from?Edit:
Here's ModalProgressView.taskView
and all of its usages:
import javafx.beans.property.SimpleDoubleProperty
import javafx.collections.ListChangeListener
import javafx.concurrent.Task
import javafx.scene.Scene
import javafx.stage.Stage
import org.controlsfx.control.TaskProgressView
fun <T : Task<*>> T.attachModalProgressUi() = apply {
runOnUiThread {
ModalProgressView.submit(this)
}
}
object ModalProgressView {
private val stage = Stage()
private const val TASK_HEIGHT = 65.0
private val targetHeight = SimpleDoubleProperty(TASK_HEIGHT)
private val taskView = TaskProgressView<Task<*>>()
init {
stage.scene = Scene(taskView)
taskView.tasks.addListener(
ListChangeListener {
targetHeight.value = TASK_HEIGHT * it.list.size.coerceAtLeast(1)
}
)
}
fun submit(task: Task<*>) {
if (task.isCancelled || task.isDone) return
task.appendOnCancelled { task.removeAndHideIfApplicable() }
.appendOnFailed { task.removeAndHideIfApplicable() }
.appendOnSucceeded { task.removeAndHideIfApplicable() }
taskView.tasks.add(task)
stage.show()
}
private fun Task<*>.removeAndHideIfApplicable() {
taskView.tasks.remove(this)
if (taskView.tasks.isEmpty()) stage.hide()
}
}
Since TaskProgressView
is from ControlsFX, its source code is available on GitHub.
- Where is the field
far
coming from?
It comes from javafx.scene.Parent
and it is used for bounds calculations. It is not a memory leak. It points to a child of the parent node.
- Is this really preventing garbage collection, or am I looking at the wrong place?
No it isn't. The problem is that the Parent
is reachable. It looks like one reachability path is via the static taskView
field of your ModalProgressView
class. But there could be other paths too. (I don't know if VisualVM will show you all of the paths.)
- If it is preventing garbage collection and ChatGPT is right, how can I prevent this from causing a memory leak?
ChatGPT doesn't understand what is going on here.
The far
field is not the cause. To fix the problem, you need to assign null
to taskView
at the appropriate time. For example, you could change
if (taskView.tasks.isEmpty()) stage.hide()
to assign null
to taskView
when the task list becomes empty. But, we can't advise on the best solution because we don't really understand the significance of these classes in the overall architecture of your application.
For example, the real problem could be that the ModalProgressView
is accumulating more and more children ... because the children are not being fully removed when the tasks complete.
Or this could be a wild goose chase. Maybe the memory leak is not going to cause problems because it doesn't increase over time. We don't know. We don't have the context. (And judging from the wording of your question, even you are not sure there is real memory leak here.)