Skip to content
Innovialabs

How Garbage Collection Works in Java, Go and JavaScript

Advanced4 min

In short

A garbage collector frees objects the program can no longer reach through references; leaks happen when an unneeded object is still reachable, for example from a cache or a subscription.

The short answer

A garbage collector (GC) automatically frees memory the program no longer uses. The key word is reachability: an object is alive as long as it can be reached through a chain of references from the roots (local variables, the stack, global and static fields). Everything unreachable is garbage.

The practical takeaway: a GC does not know you "no longer need" an object. It only knows nothing references it. That is why memory leaks are common in garbage-collected languages.

Three core approaches

Reference counting

Every object keeps a count of references to it. When the count reaches zero, the object is freed immediately.

  • Pro: memory is released predictably and right away.
  • Con: cyclic references (A points to B, B points to A) never drop to zero. CPython adds a separate cycle collector for this, and in Swift (ARC) developers break cycles with weak.

Java, Go and V8 do not use reference counting as their main mechanism.

Mark-and-sweep

  1. Mark — walk the object graph from the roots and mark everything reachable.
  2. Sweep — free everything that is not marked.

Cycles are not a problem: if a cycle cannot be reached from the roots, the whole thing is collected. Many collectors add compaction, moving live objects together to avoid fragmentation.

Generational GC

The observation behind it: most objects die young. The heap is split into a young and an old generation. The young one is collected often and cheaply (usually by copying survivors), the old one less often. An object that survives several collections is promoted to the old generation.

How the three runtimes do it

Java (HotSpot)GoJavaScript (V8)
BasisGenerations + mark/compactConcurrent tri-color mark-and-sweepGenerations + mark-sweep-compact
GenerationalYesNoYes
Moves objectsYesNoYes
TuningCollector choice, heap sizesGOGC, GOMEMLIMITV8 flags, rarely touched

Java. The JVM ships several collectors: G1 (the default in modern JDKs), the low-latency ZGC and Shenandoah, plus Parallel and Serial. Choosing one is a trade-off between pause times, throughput and memory footprint.

Go. The collector is concurrent: it runs alongside your program with short pauses. There are no generations and no moving. Instead, the compiler uses escape analysis to keep many values on the stack, where they never touch the GC. GOGC controls how often collection runs, and GOMEMLIMIT sets a soft memory limit.

JavaScript (V8). The young generation is collected by a fast copying collector (the Scavenger), the old one by mark-sweep-compact, with much of the work done incrementally and on background threads. Other engines differ in detail, but the generational idea is widespread.

How leaks still happen

A leak in a GC language is an object that is unneeded but still reachable. Typical causes:

  • Java: static collections and caches without eviction, listeners and subscriptions never removed, ThreadLocal values in thread pools.
  • Go: goroutines blocked forever on a channel, a small slice that keeps a huge backing array alive, maps that only grow.
  • JavaScript: forgotten addEventListener and setInterval calls, closures capturing large data, detached DOM nodes still referenced somewhere.

The common pattern: anything long-lived (globals, singletons, caches) needs a bound or explicit cleanup.

How to find a leak

  1. Confirm the growth. Memory measured after collections keeps climbing under steady load — that is a warning sign. A spike alone is not.
  2. Take two heap snapshots some time apart and compare which objects increased.
  3. Find the retaining path — the chain of references from a root to the unwanted object. It tells you who is holding the memory.

Tools:

  • Java: jcmd <pid> GC.heap_dump, analysis in Eclipse MAT or VisualVM, Java Flight Recorder.
  • Go: net/http/pprof with go tool pprof for the heap profile, the goroutine profile to find stuck goroutines.
  • JavaScript: the Memory tab in Chrome DevTools (heap snapshots and comparison), node --inspect for Node.js.
import _ "net/http/pprof" // adds /debug/pprof/ to your HTTP server

FAQ

Technically yes (System.gc(), runtime.GC()), but in production it almost never fixes anything. If memory keeps growing, look for retaining references instead of forcing collections.

We can help with this

If you'd rather get the result than figure it all out yourself — this is what we do every day.

Discuss a project

Read next

Have an idea or a task? Tell us about it

We'll reply within one business day and propose a solution. The consultation is free.