Skip to the content
Software Made Clear Diagrams that show the mechanism About

Memory leaks

See how lost allocations and unwanted retained references cause memory leaks, why a flat usage curve does not rule one out, and what a collector can reclaim.

4 min read Updated September 30, 2026 Last verified September 23, 2026
ANSWER

A leak can mean an allocation whose pointer was lost or an object retained after the program no longer needs it. A garbage collector can reclaim unreachable managed objects but cannot infer that a reachable object is unwanted. Neither case requires unbounded growth.

IN PLAIN TERMS

You keep putting things into a cupboard and never take anything out. Some of it you have forgotten you own and cannot find. The rest you could reach any time you liked — you simply never decide to throw it away. A tidier who works while you sleep bins what nobody can reach, but never what still looks like yours.

“Memory leak” names two failures that behave differently, are found differently, and are fixed differently. Treating them as one thing is why the advice on the subject seems to contradict itself — half of it says a garbage collector solves this and the other half says collected languages leak constantly, and both are right about the failure they have in mind. The clearest way to separate them is to borrow the distinction a leak checker already has to make.

Two different failures share the name#

Valgrind’s Memcheck has to classify every block still allocated when a program ends, and the categories it uses are the distinction this whole subject needs. A block is definitely lost when, in the manual’s own words, no pointer to the block can be found — the program could not have freed it at exit, because nothing was left holding its address. A block is indirectly lost when it is only reachable through blocks that are themselves lost, which is what a leaked tree or list looks like: one dropped pointer at the root, and everything beneath it goes with it.

Then there is the category people skip past. A block is still reachable when a pointer chain to it was found, so — again in the manual’s words — the programmer could at least in principle have freed it before exit. The memory was never returned, and it was never unreachable either. Nothing was dropped. The program simply kept hold of it.

That is the line worth carrying around, and it is a line about reachability rather than about intent. On one side is memory the program has lost the ability to free — stranded on the heap, where the program rather than the language decides when something ends. On the other is memory the program can still reach perfectly well and has no further use for. Either can be a finite leak whose memory curve flattens. Repeating either mistake can instead exhaust the machine; a collector can reclaim unreachable objects but cannot know that a reachable object is no longer useful.

Why a garbage collector cannot save you#

A tracing collector can reclaim unreachable managed objects. That avoids the lost-pointer problem for those objects, but does not mean all allocations in a collected language are managed: native libraries or unmanaged buffers can still leak. Collection timing and returning freed pages to the operating system are separate. A collector also cannot infer that a still-reachable object is unwanted.

Apply that reachability rule to the other category and it gives you nothing. An entry in a map you keep appending to is reachable. A listener registered against a long-lived object is reachable. A cache with no eviction policy is reachable, deliberately, by design. Every one of those is doing exactly what the collector requires of a live object, and the collector is correct to keep it — the program said, in the only language a collector reads, that this memory is still wanted.

This is why “we moved to a managed runtime so we no longer have leaks” is half a sentence. The half that is true is real and worth having. The half that is missing is that unwanted reachable objects can remain indefinitely. If the program retains more of them with each request, memory can scale with uptime; if it retains only a finite set, the curve can plateau without making that retention intentional.

Where it goes wrong#

Remove roots, then trace what is still reachable

A finished request no longer needs its 10 MB payload. A cache root and a listener root each still point to it. Predict which reference must go before a tracing collector can reclaim it.

Cache → payload and listener → payload: 10 MB remains reachable. Removing only one reference keeps the other path; removing both makes the payload eligible for collection.

One managed object and two strong roots, no finalizers or native allocations. Eligibility is not an immediate collection or an operating-system memory reduction. The model does not infer whether a retained object is useful.

One possible service leak is a map with insertion but no removal after a session ends. A short happy-path test may miss repeated retention; review can catch a missing lifecycle rule, and a focused repeated-work test or heap-retainer inspection can expose it. Growth over time is a symptom to investigate, not a requirement for calling an allocation leaked.

The second is diagnosing from a single measurement. A curve that climbs and then flattens often reflects a filled working set or an allocator retaining freed pages, but it can also reflect one leaked block or a finite set of unwanted retained objects. Repeated growth under steady work is a strong reason to investigate; a plateau does not settle the ownership question. Inspect live allocations and whether the program still needs them.

The third is worth naming because it inverts the usual advice: at process exit, still-reachable memory is frequently harmless. The process is ending, the operating system reclaims everything, and freeing it first buys nothing but shutdown latency. That is why Memcheck reports the category separately rather than counting it with the losses. The same pattern in a process that never exits is the whole problem — which means the question is never just “was this freed”, but “how long is this program going to run”. A long-lived service is a different program from the same code invoked once, in the way a background process that has to keep up forever is a different proposition from one that has to finish.

IF YOU REMEMBER ONE THING

A collector can reclaim managed objects that nothing can reach. The leaks that survive it are unmanaged allocations and objects your program can still reach but no longer wants — which is not a memory bug the runtime can find for you, but a decision about lifetime that only the code can state.

Questions people also ask

5 QUESTIONS
What is the difference between a leak and high memory usage?

A leak is memory retained when the program no longer needs it, whether the pointer was lost or an unwanted reference remains. Repeated retention often produces continuing growth, but one lost allocation can leave a permanent flat increase and a bounded-keyspace leak can plateau. A stable curve is useful diagnostic evidence, not proof that no memory was leaked.

Can a garbage-collected language leak memory?

Yes. A collector can reclaim unreachable managed objects, but it cannot infer that a reachable object is no longer needed. A map entry, registered listener or cache entry can therefore remain live after it has lost its purpose.

What does Valgrind's "still reachable" actually mean?

That a pointer chain to the block was found, so the program could in principle have freed it before exiting. Memcheck reports it separately from "definitely lost", where no pointer to the block exists at all. Still-reachable memory at exit is not automatically a leak; inspect whether the program still needed it before exit.

Why does the leak only show up in production?

Some leaks become visible only after enough repetitions or long uptime. A handler that retains a few kilobytes per request may look stable during a short test but consume substantial memory after many requests. A finite leak can also remain flat, so allocation and retention evidence matters more than the shape of one chart.

Do I have a leak if memory goes up and then flattens?

Possibly. A plateau often means the working set has filled or the allocator kept freed pages, but a one-time lost allocation and unwanted retention over a finite set of users can also flatten. Inspect what remains allocated and whether the program still needs it; a continuing climb under steady work is a strong clue, not the definition of a leak.

The library Browse all articles