A Place for Everything
As a program executes, it generates data. That data is read from disk or the network, input by the user, or computed from calculations. The processor stores this data in memory so that it can be quickly recalled later. Modern memory is made of a large but finite number of electronic components called transistors and capacitors. If a program keeps generating new data, it will eventually run out of these components.
Developers from the 1980s reminisce about having to squeeze their data into just a few hundred kilobytes of memory. Today we often treat memory as an unlimited resource. However, if we build programs that run indefinitely or on constrained devices like phones or graphics cards, we too must pay attention to how much memory we consume. Otherwise we risk crashes and slow execution. We can't assume that every user has a computer as capable as our own.
Where data is placed in memory depends on what kind of data it is, how long it should live, and how much is known about it ahead of time. The answers to these questions lead us into organizing memory into these four regions:
- Code memory holds the machine code instructions of an executable. These instructions are copied from disk into memory when we execute a program by double-clicking on it, running it in the shell, or starting a new process from another program. The instruction pointer register on the CPU points into this segment. Operating systems generally make this region read-only so that malicious code can't get injected atop the original code.
- Static memory is used for globals and static variables. This data often physically resides in the program's executable stored on disk and is copied into the static memory segment of the process when the executable is run. The static data has the same lifetime as the code. It is allocated at the start and stays allocated until execution finishes.
- Stack memory is used for the local variables and parameters associated with functions. The data is bound up with the function lifecycle. It is allocated as part of a stack frame when its function is called and released when the function returns. If a function is recursive, each call allocates a separate stack frame so a later call doesn't clobber the data of an earlier call. The compiler or interpreter automatically allocates and releases stack data as the program executes; the programmer has no responsibility or influence over its lifetime.
- Heap memory stores two kinds of data: data whose size can't be determined ahead of time because it depends on information that is only available at runtime, and data that is allocated by a function that needs to live beyond the function's return. A language's runtime includes a special allocation function that allocates data on the heap and returns to the caller a pointer or reference to the allocation. The data is not automatically released upon the function's return; someone or something must release it when it's no longer needed.
Each process running on a computer gets allocated a memory space that is subdivided into these four regions. A process cannot access the memory of another.
In this chapter, we focus entirely on how heap memory is managed in various programming languages. In particular, we'll examine several common strategies for releasing heap memory when the data is no longer needed. By the chapter's end, you'll be able to answer the following questions:
- What are the four strategies to managing heap memory typically found in programming languages?
- For each of these four strategies, what is expected of developers, how can things go wrong, and what is their cost?
- How do closures and collections in Rust operate when the Rust compiler expects values to have a single owner?
- How have the Rust developers abstracted away map, filter, fold, and other higher-order patterns?
You'll find that Rust's approach to managing memory is very different from the approaches you've seen in C and Java, and you won't be able to write much Rust code without developing a mental model of its behavior. We'll also look at some other features of Rust that are affected by its memory management strategy.