Every computer program needs memory. Memory is finite, so a long running program must borrow memory from the operating system when it needs it and release it once it is no longer needed.
The interesting part is who reclaims that memory, and when. Some piece of code has to figure out that a piece of memory is no longer needed and release it, and figuring that out is not trivial. A value might be passed to another function, stored somewhere longer-lived, or shared across threads, and it stays needed as long as anything still refers to it. If it is reclaimed early, we get memory corruption. If it is reclaimed too late, we get memory leaks.
The paradigms
There are two paradigms for memory management, each optimizing for different things.
The first paradigm is to let the language runtime do it. A program allocates memory when it needs it, uses it as long as required, and eventually stops referring to it. A garbage collector figures out what is no longer reachable and reclaims it. Go, Java and a lot of other languages in wide use belong to this category. You give up deciding when memory is freed, but in exchange you cannot free it too early, free it twice, or forget to free it at all. For most software this is a very good compromise. It reduces the cognitive overhead of memory management and you can stay focused on the actual problem. The leaks that come from forgetting to free something go away entirely.
The second paradigm, which must be very evident at this point, is to keep the decision to yourself. In C, you allocate and free by hand, and you own every bug that comes as a result. In Rust you do not write the frees, but you do not hand the decision to a runtime either. The compiler works out at build time where each value’s life ends, reclaims it there, and refuses to build the program if it cannot prove that this is safe. So you still get control over memory and a tighter footprint, but the effort shifts. In C you pay for it by debugging corruption. In Rust you pay for it by arranging your program in a way the compiler can verify.
Sitting in between is reference counting, which is what Swift and Python do. It is really a variant of the first answer rather than a third paradigm, and it is seldom enough on its own. Counts cannot see cycles, so a language has to deal with them some other way. Python bolts on a tracing collector that hunts for cycles. Swift does not, and instead pushes the problem back to you through weak and unowned annotations. Reference counting also has its own running cost, paid on every copy of a pointer you make and every time you drop one.
Which one should you choose? If garbage collection was free, all of us would choose a runtime that manages memory on its own. But it is not free, hence we discuss the performance penalty of GC and whether it matters.
Stack and heap
When a program needs memory it comes from one of two places, the stack or the heap. Stack memory costs the collector nothing. It grows and shrinks as functions are called and return, and the machine just moves a pointer. When the function returns the value is gone, nothing has to reclaim it. Stacks are not entirely invisible to the collector, since it has to scan them as roots to find where the live objects start, but it never has to free anything there.
... continue reading