The exact work of garbage collection and memory allocation is known to only a few. Fortunately, they are very happy to share their knowledge, and the following is based on what I learned from the erlang-questions mailing list and discussing with OTP developers.
When exchanging messages between processes, the content is always copied because there is no common heap between processes. The only exception is binaries larger than 64 bytes, where only the link is copied.
When executing code in a single process, only parts are updated. Let it analyze the tuples, as this is an example that you provided.
A tuple is actually a structure that stores references to actual data somewhere on the heap (with the exception of small integers and, possibly, another type of data that I cannot remember). When you update a tuple using, for example, setelement/3 , a new tuple is created with the replaced element, however, for all other elements, only the link is copied. There is one exception that I could never use.
The garbage collector monitors each tuple and understands when it is safe to recover any tuple that is no longer in use. Perhaps the data bound by the tuple is still in use, in which case the data itself is not collected.
As always, Erlang gives you some tools to understand exactly what is going on. The performance guide details how to use erts_debug:size/1 and erts_debug:flat_size/1 to understand the size of the data structure when used inside the process and when copying. Tracing Tools also let you know when, what, and how much garbage was collected.
source share