Spark - Cleaning Up Objects with WeakReference

Liao Jiayi Liao Jiayi #JVM#Spark#Apache Spark

A Stack Overflow question about when Spark reclaims Accumulator and Broadcast variables led me to explore how ContextCleaner uses WeakReference and a reference queue to clean up unused objects.

Translated from Chinese with AI · Read the original

I recently saw a question on Stack Overflow about when Spark reclaims variables such as Accumulators and Broadcasts. While reading the source, I came across this interesting mechanism.


Spark ContextCleaner

When Spark is idle, with no tasks running, we often see logs like these:

19/02/12 05:19:51 INFO Spark Context Cleaner org.apache.spark.internal.Logging$class.logInfo(Logging.scala:54): Cleaned accumulator 108284023
19/02/12 05:19:51 INFO Spark Context Cleaner org.apache.spark.internal.Logging$class.logInfo(Logging.scala:54): Cleaned accumulator 108283658

Tracing the source reveals that SparkContext initializes ContextCleaner on startup and launches a daemon cleaningThread. This thread loops continuously, cleaning up unused objects such as RDDs, Broadcast variables, and Accumulators.
This raises a question: taking an Accumulator as an example, how does ContextCleaner know that it is no longer used, meaning that no object references it?

First, let’s look at the cleanup process:

val reference = Option(referenceQueue.remove(ContextCleaner.REF_QUEUE_POLL_TIMEOUT))
.map(_.asInstanceOf[CleanupTaskWeakReference])
// Synchronize here to avoid being interrupted on stop()
synchronized {
reference.foreach { ref =>
logDebug("Got cleaning task " + ref.task)
referenceBuffer.remove(ref)
ref.task match {
case CleanRDD(rddId) =>
doCleanupRDD(rddId, blocking = blockOnCleanupTasks)
case CleanShuffle(shuffleId) =>
doCleanupShuffle(shuffleId, blocking = blockOnShuffleCleanupTasks)
case CleanBroadcast(broadcastId) =>
doCleanupBroadcast(broadcastId, blocking = blockOnCleanupTasks)
case CleanAccum(accId) =>
doCleanupAccum(accId, blocking = blockOnCleanupTasks)
case CleanCheckpoint(rddId) =>
doCleanCheckpoint(rddId)
}
}
}

ContextCleaner finds the object to clean up (CleanAccum) through a referenceQueue. Starting there, let’s examine WeakReference in the JVM.


Java WeakReference

Java has several kinds of references:

  • StrongReference: The objects we normally define use these references and are harder to collect.
  • WeakReference: For example, Spark wraps references in CleanupTaskWeakReference(task, objectForCleanup, referenceQueue). If the referenced object (task) is reachable only through this WeakReference, it is reclaimed during GC and placed in referenceQueue.
  • SoftReference: Stronger than a WeakReference. The referent can be reclaimed, but not necessarily during the next GC.

ContextCleaner therefore combines WeakReference with referenceQueue to reclaim objects. Registering an Accumulator also calls registerForCleanup:

/** Register an object for cleanup. */
private def registerForCleanup(objectForCleanup: AnyRef, task: CleanupTask): Unit = {
referenceBuffer.add(new CleanupTaskWeakReference(task, objectForCleanup, referenceQueue))
}

referenceBuffer prevents the WeakReference itself from being collected before it is processed.

Spark wraps the registered Accumulator in a CleanupTask and initializes a WeakReference based on that task. Once the Accumulator is no longer referenced, the task is placed in referenceQueue. The cleaningThread retrieves the object awaiting collection from the queue and processes it, as shown in the cleanup code above.