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.
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.
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.
Ownership in Rust is a set of rules that the compiler checks at compile-time to ensure memory safety. These rules are:
Rc and Arc, support a different pattern.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.
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.RustIn 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.
A String move is easier to follow with a model of the stack and heap:
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");RustWhen 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 │ │ │
└─────────┘ └─────────┘PlaintextAfter moving s1 to s2:
let s2 = s1;RustThe 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.PlaintextThe move leaves one active owner responsible for the string buffer. This prevents a double free while still allowing references that satisfy the borrowing rules.
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);RustThis 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 │
└─────────┘ PlaintextCopy 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.
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.PlaintextOwnership 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);
}RustIn 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.
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);
}RustIn 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.RustA 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.

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.
Scala, JVM, AI, and backend systems. I send practical articles when there is something worth reading.
Join the newsletter