Turn your question around. The real motivating question is under what circumstances can we avoid the costs of garbage collection?
Well, first off, what are the costs of garbage collection? There are two main costs. First, you have to determine what is alive; that requires potentially a lot of work. Second, you have to compact the holes that are formed when you free something that was allocated between two things that are still alive. Those holes are wasteful. But compacting them is expensive too.
How can we avoid these costs?
Clearly if you can find a storage usage pattern in which you never allocate something long-lived, then allocate something short-lived, then allocate something long-lived, you can eliminate the cost of holes. If you can guarantee that for some subset of your storage, every subsequent allocation is shorter-lived than the previous one in that storage then there will never be any holes in that storage.
But if we've solved the hole problem then we've solved the garbage collection problem too. Do you have something in that storage that is still alive? Yes. Was everything allocated before it longer-lived? Yes -- that assumption is how we eliminated the possibility of holes. Therefore all you need to do is say "is the most recent allocation alive?" and you know that everything is alive in that storage.
Do we have a set of storage allocations where we know that every subsequent allocation is shorter-lived than the previous allocation? Yes! Activation frames of methods are always destroyed in the opposite order that they were created because they are always shorter-lived than the activation which created them.
Therefore we can store activation frames on the stack and know that they never need to be collected. If there is any frame on the stack, the entire set of frames below it is longer-lived, so they don't need to be collected. And they will be destroyed in the opposite order that they were created. The cost of garbage collection is thus eliminated for activation frames.
That's why we have the temporary pool on the stack in the first place: because it is an easy way of implementing method activation without incurring a memory management penalty.
(Of course the cost of garbage collecting the memory referred to by references on the activation frames is still there.)
Now consider a control flow system in which activation frames are not destroyed in a predictable order. What happens if a short-lived activation can give rise to a long-lived activation? As you might imagine, in this world you can no longer use the stack to optimize away the need to collect activations. The set of activations can contain holes again.
C# 2.0 has this feature in the form of yield return
. A method that does a yield return is going to be reactivated at a later time -- the next time that MoveNext is called -- and when that happens is not predictable. Therefore the information that would normally be on the stack for the activation frame of the iterator block is instead stored on the heap, where it is garbage collected when the enumerator is collected.
Similarly, the "async/await" feature coming in the next versions of C# and VB will allow you to create methods whose activations "yield" and "resume" at well-defined points during the action of the method. Since the activation frames are no longer created and destroyed in a predictable manner, all the information that used to be stored in the stack will have to be stored in the heap.
It is just an accident of history that we happened to decide for a few decades that languages with activation frames that are created and destroyed in a strictly ordered manner were fashionable. Since modern languages increasingly lack this property, expect to see more and more languages that reify continuations onto the garbage-collected heap, rather than the stack.