← Runtimes
Runtime walkthrough 7 questions · 6 languages

Stack and heap

A correct model of calls, refined for how long values last.

A call ending and a value being freed are different events. The stack-and-heap picture explains when a call ends. In Rust, the owner decides the rest: a value is dropped, and its heap memory freed, when its owner goes out of scope, which can be long after the call that created it.

Before you begin

Why this matters

In Rust, knowing what lives in a frame and what an owner keeps on the heap explains which references the compiler accepts, and when memory is freed.

What this helps you reason about

  • Predict whether a value is still usable after the call that created it returns.
  • Explain why returning a Box or String works but returning a reference to a local does not.
  • Say when Drop runs and which binding owns a value.

The stack-and-heap picture is a correct, useful model of how calls work, and Rust fits it closely. This lesson uses the picture to predict real behavior, then adds one idea the picture does not have: an owner, the one binding responsible for a value, which decides when its heap memory is freed.

Already know this? The short version
  • Each call has a frame that disappears on return. An i32 and other small Copy values live in it, while Box, String, and Vec keep their contents on the heap.
  • Every heap value has one owner. Returning or assigning a Box or String moves ownership; only the small part (pointer, length, capacity) moves, and the old binding can no longer be used.
  • A reference cannot outlive its value, so returning &local is rejected: error E0106 without a lifetime, and “returns a reference to data owned by the current function” with one.
  • When the owner goes out of scope, Drop runs and frees the memory at that exact point. Rc and Arc count owners, and leaks are still possible through Rc cycles or std::mem::forget.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
frame
The record of one function call that has not finished: its parameters and local variables. It is created when the call starts and removed when the call returns.
heap
An area of memory for data whose size or lifetime is decided while the program runs, so it can stay usable after the call that created it returns. Box, String, and Vec keep their contents there.
Box
A small value that owns one value stored on the heap. Box::new(3) puts 3 on the heap and returns the Box that owns it.
owner
The one binding, struct field, or collection that holds a value and is responsible for freeing it. A value has exactly one owner at a time. When the owner goes out of scope, Rust drops the value and frees its memory.
move
Assigning, passing, or returning a value whose type is not Copy, such as a String. Ownership goes to the new binding, and the compiler rejects any later use of the old one.
drop
What Rust does to a value when its owner goes out of scope: it runs the value’s cleanup code, if its type has any, and frees any memory the value owns elsewhere, such as a String’s text or a Box’s contents. A type provides cleanup code by implementing the Drop trait.
reference
A value written &x that lets code use x without owning it. The compiler checks that a reference is never used after x is gone.
lifetime
The part of the program during which a reference may be used. It must end before the value the reference points to is dropped.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / The common picture

What do people mean by stack and heap?

Here is the picture most programmers learn. It is a teaching model: a simple picture that explains real behavior. Each function call gets a frame, a block of memory for its parameters and local variables. The frames are kept on the stack, also called the call stack. A new call adds a frame on top of the stack, and returning removes that frame.

Some values must stay usable after the call that created them returns, for example an object that the call gives back to its caller. A frame cannot hold such a value, because the frame is removed when the call returns. So those values are kept in a separate area of memory, called the heap, until the program no longer needs them.

In Rust, small values such as an i32 (a 32-bit integer) usually live in the frame. Types such as Box, String, and Vec keep their contents on the heap.

This model is correct for many programs. The rest of this lesson uses it to predict behavior, then refines it for the cases it does not cover.

Frames hold one call’s variables; the heap holds what a Box, String, or Vec owns.

02 / Frames

What happens to a call’s variables when it returns?

A frame exists only while its call is running. When inner returns, its frame is removed and doubled is gone. outer receives only the returned value.

main.rs
fn inner(n: i32) -> i32 {
    let doubled = n * 2;
    doubled
}

A call’s local variables end when it returns; its return value does not.

Experiment

Watch a frame come and go

Each call gets a frame holding its parameters and local variables. A frame exists only while its call is running. When inner returns, its frame is removed; only the returned value reaches outer.

Why it matters Local variables belong to one call. Once the call returns, the only thing the caller has is the value that was returned.

Predict what prints. Step until inner is running, then step once more. What happens to doubled?

fn inner(n: i32) -> i32 {
    let doubled = n * 2;
    doubled
}

fn outer() -> i32 {
    let start = 4;
    let result = inner(start);
    result + 1
}

fn main() {
    println!("{}", outer());
}
What to look for

Watch the frame list. outer appears, then inner on top of it. When inner returns, its frame and its doubled variable are gone, and outer continues with result.

Expected output

9

03 / Outliving a call

How can a value last longer than the call that made it?

The Variables lesson showed that assigning a Box or String moves it to a new owner; returning one moves it the same way. Box::new(3) puts the 3 on the heap, owned by the Box. Returning the Box moves ownership to the caller, so the 3 outlives the frame that created it. Each call makes a new Box.

When the owner goes out of scope, at the end of its block, Rust drops the value. Dropping runs the value’s cleanup code, called Drop, and for a Box that frees the heap memory. Section 05 returns to this.

main.rs
fn make_counter() -> Box<i32> {
    Box::new(3)
}
let mut first = make_counter();
*first = 9;
let second = make_counter(); // a new Box holding 3

Two returned Boxes. This is a model of who owns each value, not of memory addresses.

A conceptual model, not a physical memory layout or live code execution.

1 / 4

Create

make_counter: Box
owns A
A
3

Box::new puts 3 on the heap, owned by the Box.

Experiment

Move a Box out of its call

Box::new(3) puts the 3 in memory owned by the Box, not in the frame. Returning the Box moves ownership to the caller, so the 3 stays usable after the frame is gone. Each call makes a new Box.

Why it matters The owner decides how long heap data lives. When first and second go out of scope at the end of main, each Box frees its own memory.

Predict both numbers. Then change let second = make_counter(); to let second = first; and read the compiler error.

fn make_counter() -> Box<i32> {
    Box::new(3)
}

fn main() {
    let mut first = make_counter();
    *first = 9;
    let second = make_counter();
    println!("{} {}", first, second);
}
What to look for

Step past the first return. The make_counter frame is gone, and first shows an address followed by → 3, then → 9: the Box’s memory on the heap, now owned by first. The second call makes a second Box at a different address, holding 3, and 9 3 prints. GDB stops twice on line 2 in each call. After the edit, the compiler reports error[E0382]: borrow of moved value: `first`, at the print: the Box moved to second.

Expected output

9 3

04 / Refining the picture

Where does the picture need refining?

Here the picture needs refining. It says nothing about who frees heap memory, or about references. Rust adds both. A String is a small value in the frame (a pointer, a length, and a capacity) plus its text on the heap. Returning a String moves the small value; the text stays where it is.

A reference, written &label, lets code use label without owning it. The part of the program where a reference may be used is called its lifetime, and it must end before label is dropped. label ends with its frame, so a returned reference to it would point at nothing. The compiler rejects that function.

main.rs
fn make_label() -> &String {
    let label = String::from("ready");
    &label // rejected: label ends here
}

Return owned values out of a call, not references to its locals.

Coming from C: In C, returning the address of a local compiles with a warning and gives a dangling pointer; rustc refuses to compile it.

Precisely: the compiler’s error

rustc, the Rust compiler, reports “missing lifetime specifier” for this signature. With a lifetime added, it reports that the function “returns a reference to data owned by the current function”. Both say the same thing: the reference would outlive label.

Experiment

Move a String without moving its text

A String has two parts: a small value with a pointer, length, and capacity, and the text itself on the heap. Returning the String moves the small value out of the frame; the text stays where it is. A reference to label could not be returned, because label itself ends with the frame.

Why it matters Moving a String is cheap because only the small part moves. The compiler rejects references that would outlive the value they point to.

Predict what prints. Then change make_label to return &String and end it with &label. Read the compiler error.

fn make_label() -> String {
    let label = String::from("ready");
    let length = label.len();
    println!("made {} bytes", length);
    label
}

fn main() {
    let label = make_label();
    println!("{}", label);
}
What to look for

Compare the data= address of label inside make_label with the data= address after it returns to main. They are the same: the text did not move, only the small String value did. After the edit, the compiler reports error[E0106]: missing lifetime specifier: the function would return a reference to label, which is dropped when make_label returns.

Expected output

made 5 bytes
ready

05 / Releasing memory

Who decides when memory can be reused?

Rust has no garbage collector, a background process that other languages use to find and free unused memory. Every heap value has exactly one owner. When the owner goes out of scope, Rust runs the value’s Drop code, which frees the memory. You do not call free yourself.

Two more rules decide the exact line. Values in one scope are dropped in reverse order of declaration, the newest first. And giving a variable a new value drops its old value at that line.

Sometimes a value needs several owners. Rc, short for reference counted, keeps a count of the owners and frees the value when the last one goes away. Arc, short for atomically reference counted, does the same and can also be shared between threads; Rc is for one thread only.

The owner going out of scope frees the memory, at a predictable point.

Coming from JavaScript: JavaScript frees memory some time after a value becomes unreachable; Rust frees it at the exact line where the owner goes out of scope.

Precisely: leaks are still possible

Safe Rust prevents using freed memory, but it does not promise that every allocation is freed. std::mem::forget and cycles of Rc values can leak memory.

06 / The implementation

What may the compiler or runtime change?

The compiler, rustc, translates your Rust into machine instructions. When it optimizes, it may keep values in registers, small storage places inside the processor itself, instead of in a frame. It may also merge frames by inlining calls, which means copying a function’s body into its caller. It may even skip a heap allocation it can prove is unnecessary. A program that follows Rust’s rules behaves the same in every case. Safe Rust always follows them; code inside an unsafe block must be checked by you.

Ownership describes behavior; it does not promise a particular address.

Precisely: what the stepper shows

The stepper compiles with rustc at opt-level 0, which means no optimization, and with debug info for the debugger. The rustc version is the one that Debian packages. The data= address it shows for a String is the real heap address in that run. An optimized build, such as cargo build --release, may store values differently.

07 / Try it

When can each value’s memory be released?

Predict the output, then name the line where each String’s text is freed. No garbage collector is involved.

main.rs
fn make_label(text: &str) -> String {
    String::from(text)
}

fn main() {
    let mut current = make_label("draft");
    let kept = make_label("final");
    println!("{}", current);
    current = make_label("second draft");
    {
        let note = make_label("note");
        println!("{} {}", current, note);
    }
    println!("{}", kept);
}

What prints? At which line is the text of each String freed: draft, final, second draft, and note?

Reveal the reasoning

draft, then second draft note, then final. draft is freed at line 9: giving current a new value drops its old value at that line. note is freed at line 13, where its block ends. final and second draft are freed where main ends, at line 15: kept is dropped first, then current, because values in one scope are dropped in reverse order of declaration. Each line is decided when the program is compiled; nothing waits for a collector.

Practice tickets

  1. STK-101 · Keep each shelf label as it was printed → Free
  2. STK-102 · Hand out a receipt snapshot that stays as it was → Sign in · Pro
  3. STK-103 · Open one connection and close it → Sign in · Pro

Keep exploring

Investigate a minimal reproduction →

Primary references