Description
Hello 👋
This issue is introduced in version 2.1.15
with #9500.
The root cause is this part:
override def toString: String = s"Ref.Atomic(initial = $initial)"
This usage of initial
inside final class Atomic[A]
prevents it from being garbage collected. The only other usage is inside val unsafe = new AtomicReference[A](initial)
, but that is mutable so if it wasn't for such toString
implementation, it would have eventually be garbage collected after the referenced value is changed using any of the mutation methods.
It might not be a problem for cases when the initial value for Ref
is not a large object, but in our case we happen to initiate it with very large values. The fact that those values are kept in memory even after the ref is modified, makes us waste gigabytes of memory with data that we will never access.
The previous toString
version used to print the current value:
override def toString: String =
s"Ref(${value.get})"
@hearnadam argues that this is unnecessary, which I can relate to, especially since it's not suspended. However, I think printing the initial value is arguably even less useful. It is possible to keep this behaviour it by memoizing initial.toString
though:
private[zio] final class Atomic[A](initial: A) extends Ref[A] { self =>
private val initialStr = initial.toString
...
override def toString: String = s"Ref.Atomic(initial = $initialStr)"
...
}
I think it's fair to assume that in many cases the string representation of the initial
will be considerably smaller than the actual value. But this way the initial value can be garbaged collected once the ref is modified, so there is much less memory usage. Alternatively, toString
could be simplified even further and just not print any content of the wrapped value (initial or current). I guess the old behavior can also be brought back with usafe.get
, though I agree with @hearnadam that it's not worth it