Understanding Ownership in Rust: A Deep Dive

Follow a String through moves, clones, function calls, and borrowing. Stack-and-heap diagrams connect Rust’s ownership rules to the memory-management model familiar to JVM developers.

Introduction to Ownership

Rust ownership defines who is responsible for a value and when its resources are released. For a developer used to Scala or Java, the main shift is learning when a value moves and when a function only borrows it. This guide follows String values through those operations; Rust strings explains the underlying owned and borrowed text types.

Coming from a background in Scala and Java, where the JVM takes care of memory management, the concept of ownership can seem daunting at first. In these languages, you don’t usually deal with memory explicitly; the garbage collector does the heavy lifting. The last time many developers, including myself, explicitly managed memory was back in the days of C or C++, often during academic projects. After experiencing the convenience of languages with garbage collection, the idea of going back to manual memory management can be unappealing. However, ownership in Rust provides a way to manage memory safely and efficiently without the drawbacks of manual memory management or the overhead of a garbage collector.

The Basics of Ownership

Ownership in Rust is a set of rules that the compiler checks at compile-time to ensure memory safety. These rules are:

  1. Each value in Rust has a variable that’s called its owner.
  2. A normal move transfers ownership rather than creating a second independent owner. Explicit shared-ownership types, such as Rc and Arc, support a different pattern.
  3. When an owner leaves its scope normally, its value is dropped. For an owned String, this releases the string buffer.

These rules make Rust different from other languages and enable the compiler to provide informative error messages that help you write safer code.

Ownership in Action

Let’s take a look at an example to understand how ownership works in Rust:

let s1 = String::from("abc");
let s2 = s1;
println!("{}", s1); // This will not compile. Rust will throw an error.
Rust

In this example, s1 owns a String. Assigning it to s2 moves the value and invalidates the original binding. The compiler rejects the later attempt to print s1 because it uses a moved value.

For String, moving transfers responsibility for releasing the allocation. Invalidating the old binding prevents both bindings from independently freeing the same buffer. Borrowed references can still refer to the value under Rust’s borrowing rules.

Behind the Scenes: Stack and Heap

A String move is easier to follow with a model of the stack and heap:

Stack

  • Fast
  • In-Order
  • Fixed Size
  • Last-In-First-Out (LIFO)

Heap

  • Slower
  • Out-of-Order
  • Variable Size
  • Unordered

When you create a String in Rust, the value is stored in two parts: the stack and the heap. Here’s how it works:

let s1 = String::from("abc");
Rust

When s1 is created, Rust stores the pointer, length, and capacity of the string on the stack, while the actual string data (“abc”) is stored on the heap. Here’s a simplified ASCII diagram to illustrate this:

Stack                     Heap
┌─────────┐              ┌─────────┐
│ s1.ptr  │ ───────────> │ "abc"   │
│ s1.len  │              │         │
│ s1.cap  │              │         │
└─────────┘              └─────────┘
Plaintext

After moving s1 to s2:

let s2 = s1;
Rust

The pointer, length, and capacity are copied to s2, and s1 becomes invalid. Here’s how it looks after the move:

Stack                     Heap
┌─────────┐              ┌─────────┐
│ s2.ptr  │ ───────────> │ "abc"   │
│ s2.len  │              │         │
│ s2.cap  │              │         │
└─────────┘              └─────────┘

s1 is now invalid and cannot be used.
Plaintext

The move leaves one active owner responsible for the string buffer. This prevents a double free while still allowing references that satisfy the borrowing rules.

Copying vs. Cloning

If you want to duplicate the value of s1 without invalidating it, you can use the clone method:

let s1 = String::from("abc");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
Rust

This works because clone performs a deep copy of the heap data, creating a new String with its own heap memory. Here’s how it looks after cloning:

Stack                     Heap
┌─────────┐              ┌─────────┐
│ s1.ptr  │ ───────────> │ "abc"   │
│ s1.len  │              └─────────┘
│ s1.cap  │              
└─────────┘              

┌─────────┐              ┌─────────┐
│ s2.ptr  │ ───────────> │ "abc"   │
│ s2.len  │              └─────────┘
│ s2.cap  │              
└─────────┘              
Plaintext

Copy is a trait for values that can be duplicated implicitly by copying their bits. Integers and shared references are examples. Clone is explicit and its behavior depends on the type; cloning a String duplicates its text buffer. See the Copy and Clone documentation.

Dropping Values

When an owned value leaves scope normally, Rust runs its destructor and releases resources according to the type’s implementation. A String releases its heap buffer. This is not a guarantee that all memory leaks are impossible: reference cycles and mem::forget can keep resources from being released.

Here’s an illustration of what happens when a value is dropped:

Stack                     Heap
┌─────────┐              ┌─────────┐
│ s1.ptr  │ ───────────> │ "abc"   │   Value goes out of scope
│ s1.len  │              └─────────┘   and is dropped.
│ s1.cap  │               
└─────────┘               

Stack is popped, and heap memory is freed.
Plaintext

Ownership and Functions

Ownership also affects how you pass variables to functions. Consider the following example:

let s1 = String::from("abc");
do_something(s1);
println!("{}", s1); // This will not compile. Rust will throw an error.

fn do_something(s: String) {
    println!("{}", s);
}
Rust

In this case, s1 is moved into the function do_something, which means s1 is no longer valid in the calling function after the move. If you try to use s1 after calling do_something, Rust will throw a compile-time error.

Managing Ownership with References

The idiomatic way to handle such cases in Rust is to use references, which allow you to pass a value without transferring ownership:

let s1 = String::from("abc");
do_something(&s1);
println!("{}", s1);

fn do_something(s: &String) {
    println!("{}", s);
}
Rust

In this example, &s1 is a reference to s1, meaning that s1 retains ownership, and the function do_something can still use the value. This allows you to borrow the value temporarily without taking ownership of it, a key feature of Rust that enables safe and efficient memory management.

Here’s an ASCII diagram to illustrate how references work:

Stack                     Heap
┌─────────┐              ┌─────────┐
│ s1.ptr  │ ───────────> │ "abc"   │
│ s1.len  │              └─────────┘
│ s1.cap  │              
└─────────┘              

┌─────────┐
│ &s1.ptr │ ───────────> Points to the same
└─────────┘              heap memory as s1.
Rust

Conclusion

A move transfers ownership, String::clone creates another text buffer, and a reference lets a function borrow the existing value. Next, use references and borrowing to understand shared and mutable access. For the contrasting JVM model, see ZGC and G1 garbage collection.

Share
Prabhat Kashyap

Prabhat Kashyap

Senior Technical Architect at HCLTech · working with Leonteq Security AG

I have 10+ years of experience building distributed systems and fintech platforms. I write about practical, non-obvious engineering details that official documentation often skips.

Newsletter

Get new engineering deep dives

Scala, JVM, AI, and backend systems. I send practical articles when there is something worth reading.

Join the newsletter