Understanding Memory Management in Rust: A Comparative Insight with C++ and Java/Kotlin

Cicero Hellmann
9 min readMar 29, 2024

tl;dr: This guide explores memory management across three major programming languages: Rust, C++, and Java/Kotlin. It highlights Rust’s unique ownership system that ensures safety without a garbage collector, contrasts it with C++’s manual memory management that prioritizes control and efficiency, and introduces Java/Kotlin’s garbage-collected environment which abstracts memory management to enhance developer productivity. Through an introductory lens, this article provides insights into the complexities of manual and automated memory management.

Memory management is a crucial aspect of programming that significantly impacts the performance and safety of applications. C++ has been traditionally celebrated for its detailed control over memory allocation and management. However, Rust, a modern contender, presents an innovative approach to memory management with its ownership model. Java and Kotlin offer a different perspective with their automatic garbage collection, simplifying memory management at the cost of direct control.

This article aims to illuminate memory allocation practices in Rust, drawing parallels and contrasts not only with C++ but also with Java/Kotlin.

Table of Contents

  • The Traditional Approach: Memory Management in C++
  • The Garbage-Collected Route: Memory Management in Java/Kotlin
  • Rust’s Innovative Approach: Ownership and Memory Safety
  • Ownership and Moves in Rust
  • Borrowing and References in Rust
  • References in Rust
  • Rust vs. C++ vs. Java/Kotlin: Memory Layout and Performance a Comparative
  • Final Thoughts
  • If You Are Looking To Learn Rust

The Traditional Approach: Memory Management in C++

C++ offers programmers precise control over memory management, not only through traditional means like malloc, new, and delete operators but also via modern features such as smart pointers (std::unique_ptr, std::shared_ptr). These smart pointers automate memory management tasks, significantly reducing manual effort and minimising risks associated with memory leaks and dangling pointers. This evolution in C++ practices reflects the language's adaptability, merging manual control with safety mechanisms.

Memory in C++ can be allocated on either the stack or the heap. Stack allocation is the default for primitive and local variables. For more dynamic scenarios, heap allocation is used, necessitating manual intervention to allocate and free memory.

This explicit control allows for highly efficient and optimised applications but introduces the complexity and risks of memory errors, such as leaks and dangling pointers, due to improper deallocation or usage of deallocated memory.

The Garbage-Collected Route: Memory Management in Java/Kotlin

Unlike C++ and Rust, Java and Kotlin abstract away the complexities of manual memory management through an automatic garbage collection (GC) mechanism. This system periodically identifies and discards objects that are no longer in use, freeing up memory resources without direct developer intervention. This model significantly reduces the risk of memory leaks and dangling pointers, common issues in languages requiring manual memory management.

However, the abstraction comes at a cost. The garbage collector introduces overhead that can affect application performance, especially in memory-intensive scenarios. Developers have limited control over when and how the garbage collection runs, which can lead to unpredictable application behavior in terms of performance.

Rust’s Innovative Approach: Ownership and Memory Safety

Rust revolutionises memory management with its ownership model, a system ensuring memory safety without necessitating a garbage collector. Central to this model are the concepts of ‘ownership’ and ‘borrowing,’ which together prevent common bugs found in manual memory management systems. Rust also provides mechanisms for shared ownership through types like Rc (Reference Counted) and Arc (Atomic Reference Counted), facilitating safe data access across multiple owners in single and multi-threaded contexts, respectively. These features exemplify Rust's balance between performance, safety, and developer ergonomics.

The ownership system is built around three core rules:

  1. Each value in Rust has a variable that’s called its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

This model eliminates common bugs found in languages like C++ by ensuring memory is automatically cleaned up once an object’s owner goes out of scope. Consequently, it sidesteps the pitfalls of manual memory management while maintaining the performance benefits.

Ownership and Moves in Rust

Because variables are in charge of freeing their own resources, resources can only have one owner. This prevents resources from being freed more than once. Note that not all variables own resources (e.g. references).

When doing assignments (let x = y) or passing function arguments by value (foo(x)), the ownership of the resources is transferred. In Rust-speak, this is known as a move.

After moving resources, the previous owner can no longer be used. This avoids creating dangling pointers.

// This function takes ownership of the heap allocated memory
fn destroy_box(c: Box<i32>) {
println!("Destroying a box that contains {}", c);

// `c` is destroyed and the memory freed
}

fn main() {
// _Stack_ allocated integer
let x = 5u32;

// *Copy* `x` into `y` - no resources are moved
let y = x;

// Both values can be independently used
println!("x is {}, and y is {}", x, y);

// `a` is a pointer to a _heap_ allocated integer
let a = Box::new(5i32);

println!("a contains: {}", a);

// *Move* `a` into `b`
let b = a;
// The pointer address of `a` is copied (not the data) into `b`.
// Both are now pointers to the same heap allocated data, but
// `b` now owns it.

// Error! `a` can no longer access the data, because it no longer owns the
// heap memory
//println!("a contains: {}", a);
// TODO ^ Try uncommenting this line

// This function takes ownership of the heap allocated memory from `b`
destroy_box(b);

// Since the heap memory has been freed at this point, this action would
// result in dereferencing freed memory, but it's forbidden by the compiler
// Error! Same reason as the previous Error
//println!("b contains: {}", b);
// TODO ^ Try uncommenting this line
}

Borrowing and References in Rust

Most of the time, we’d like to access data without taking ownership over it. To accomplish this, Rust uses a borrowing mechanism. Instead of passing objects by value (T), objects can be passed by reference (&T).

The compiler statically guarantees (via its borrow checker) that references always point to valid objects. That is, while references to an object exist, the object cannot be destroyed.

// This function takes ownership of a box and destroys it
fn eat_box_i32(boxed_i32: Box<i32>) {
println!("Destroying box that contains {}", boxed_i32);
}

// This function borrows an i32
fn borrow_i32(borrowed_i32: &i32) {
println!("This int is: {}", borrowed_i32);
}

fn main() {
// Create a boxed i32 in the heap, and a i32 on the stack
// Remember: numbers can have arbitrary underscores added for readability
// 5_i32 is the same as 5i32
let boxed_i32 = Box::new(5_i32);
let stacked_i32 = 6_i32;

// Borrow the contents of the box. Ownership is not taken,
// so the contents can be borrowed again.
borrow_i32(&boxed_i32);
borrow_i32(&stacked_i32);

{
// Take a reference to the data contained inside the box
let _ref_to_i32: &i32 = &boxed_i32;

// Error!
// Can't destroy `boxed_i32` while the inner value is borrowed later in scope.
eat_box_i32(boxed_i32);
// FIXME ^ Comment out this line

// Attempt to borrow `_ref_to_i32` after inner value is destroyed
borrow_i32(_ref_to_i32);
// `_ref_to_i32` goes out of scope and is no longer borrowed.
}

// `boxed_i32` can now give up ownership to `eat_box` and be destroyed
eat_box_i32(boxed_i32);
}

References in Rust

For pointers, a distinction needs to be made between destructuring and dereferencing as they are different concepts which are used differently from languages like C/C++.

  • Dereferencing uses *
  • Destructuring uses &, ref, and ref mut

Dereferencing is the operation of accessing the data to which a pointer (or a reference) points. In Rust, you use the `*` operator to dereference a pointer. This means you’re accessing the value that the pointer references.

Destructuring is a method to break apart a compound data type (like a tuple, array, or struct) into its constituent parts, often for easier access to its individual values or for pattern matching.
- In Rust, `&`, `ref`, and `ref mut` can be used in destructuring to bind references to the parts of the data being destructured.

fn main() {
// Assign a reference of type `i32`. The `&` signifies there
// is a reference being assigned.
let reference = &4;

match reference {
// If `reference` is pattern matched against `&val`, it results
// in a comparison like:
// `&i32`
// `&val`
// ^ We see that if the matching `&`s are dropped, then the `i32`
// should be assigned to `val`.
&val => println!("Got a value via destructuring: {:?}", val),
}

// To avoid the `&`, you dereference before matching.
match *reference {
val => println!("Got a value via dereferencing: {:?}", val),
}

// What if you don't start with a reference? `reference` was a `&`
// because the right side was already a reference. This is not
// a reference because the right side is not one.
let _not_a_reference = 3;

// Rust provides `ref` for exactly this purpose. It modifies the
// assignment so that a reference is created for the element; this
// reference is assigned.
let ref _is_a_reference = 3;

// Accordingly, by defining 2 values without references, references
// can be retrieved via `ref` and `ref mut`.
let value = 5;
let mut mut_value = 6;

// Use `ref` keyword to create a reference.
match value {
ref r => println!("Got a reference to a value: {:?}", r),
}

// Use `ref mut` similarly.
match mut_value {
ref mut m => {
// Got a reference. Gotta dereference it before we can
// add anything to it.
*m += 10;
println!("We added 10. `mut_value`: {:?}", m);
},
}
}

Rust vs. C++ vs. Java/Kotlin: Memory Layout and Performance a Comparative

Rust’s ownership, borrowing, and slices dictate how data is laid out in memory and accessed. While C++ offers programmers precise control over memory, Rust automates memory management without sacrificing performance, thanks to its compile-time checks that enforce memory safety without runtime overhead. Conversely, Java and Kotlin employ a garbage collection mechanism that abstracts away the complexity of memory management. This approach simplifies development but can introduce performance unpredictability due to garbage collection pauses and the overhead of managing the lifecycle of objects automatically.

The key difference between these languages lies in their approach to balancing performance with developer overhead.

Rust’s memory management, with its ownership and borrowing rules, offers a compelling model for creating safe and efficient applications. This system, while eliminating many common pitfalls of manual memory management and the performance unpredictability of garbage collection, is not a one-size-fits-all solution. For instance, C++ remains the language of choice for scenarios demanding ultimate control over system resources or legacy codebase integration. Meanwhile, Java/Kotlin’s garbage collection offers a high productivity environment for developing complex applications with less concern for memory management intricacies. Each language, with its unique approach to memory management, caters to different project requirements and developer preferences, underscoring the importance of selecting the right tool for the task at hand.

Its system of ownership, borrowing, and slices is a significant advancement in the quest for memory-safe development. On the other hand, Java and Kotlin offer a more managed environment, prioritising ease of use and safety over the fine-grained memory control provided by C++ and the performance optimisations achievable in Rust. This contrast highlights the diverse strategies programming languages use to tackle the challenges of efficient and safe memory management.

Final Thoughts

For those that have never seen the concept of manual memory management, Rust offers a compelling model that brilliantly balances safety and performance.

Understanding the memory management principles of Rust, alongside the manual intricacies of C++ and the automated ease of Java/Kotlin, equips developers with a comprehensive toolkit.

This knowledge should enable developers to make safer decisions and investigate areas that may not have been so welcoming before. Whether you’re a seasoned C++ programmer, a Java/Kotlin developer accustomed to relying on garbage collection, or completely new to the subject, exploring Rust’s ownership model offers insights into a revolutionary paradigm that redefines memory management.

This comparative insight into Rust, C++ and Java/Kotlin not only serves as an example to the diverse strategies employed by programming languages to manage memory efficiently and safely but also underscores the continuous evolution and innovation within the field of software development. Each language offers unique advantages and challenges, providing valuable lessons and tools for developers seeking to master the complexities of memory management in their projects.

If You Are Looking To Learn Rust

Whether you’re starting from scratch or looking to deepen your understanding, here are some invaluable resources to guide your path:

  • The Official Rust Programming Language Website: Start your journey with the foundational stone. This official page offers a wealth of resources, including documentation, tutorials, and community links, to get you started with Rust.
  • A Gentle Introduction to Rust: Steve Donovan’s gentle guide is perfect for beginners. It breaks down the basics of Rust programming in an easily digestible format, making it an excellent first step for newcomers.
  • The Rust Programming Language Book: Often referred to as “The Book,” this comprehensive guide is written by the developers of Rust. It covers everything from fundamental concepts to advanced features, offering a deep dive into the language.
  • Rust By Example: For those who learn best through doing, Rust By Example presents a collection of runnable examples that illustrate various Rust concepts and standard library functionalities. It’s a hands-on way to explore the language.
  • Rustlings: This series of tiny exercises is designed to get you used to reading and writing Rust code. It’s a fun, interactive way to gradually improve your Rust skills through practice and repetition.

--

--

Cicero Hellmann

Software developer by trade, Kotlin enthusiast, and game dev aspirant. Currently mastering Rust and shaping the future of mobile apps at Quartett Mobile.