Память и сборщик мусора в Rust: как язык без GC управляет ресурсами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему Rust не нуждается в сборщике мусора, но при этом безопасен?

Когда разработчики, привыкшие к Python, Java или C#, впервые слышат о Rust, их часто пугает отсутствие привычного сборщика мусора (GC). «Как же так? — думают они. — Без GC мы вернёмся в эпоху ручного управления памятью, утечек и segmentation fault?»

Спойлер: нет, не вернёмся. Rust предлагает третий путь — систему владения (ownership), которая гарантирует безопасность памяти на этапе компиляции. Никакого рантайма, никакого GC, никаких висячих указателей. Только чёткие правила и компилятор, который не даст вам выстрелить себе в ногу.

В этой статье мы разберём, как устроена память в Rust, что такое RAII, владение, заимствование и время жизни, и почему всё это делает Rust одним из самых надёжных языков системного программирования.

Как устроена память в Rust: стек и куча

Как и в любом языке низкого уровня, память в Rust делится на два основных региона:

  • Стек (stack) — быстрая, ограниченная по размеру память, работающая по принципу LIFO. Здесь хранятся локальные переменные, аргументы функций и возвращаемые адреса.
  • Куча (heap) — более медленная, но гибкая память, используемая для данных, размер которых неизвестен на этапе компиляции (например, String, Vec, Box).

В C++ вы сами решаете, когда выделять и освобождать кучу (через new/delete). В Java или Python за вас это делает сборщик мусора, периодически останавливая программу. Rust же использует систему владения, которая автоматически освобождает память, как только переменная выходит из области видимости.

Пример: базовое выделение памяти

fn main() {
    let s = String::from("Привет, мир!"); // выделение памяти в куче
    println!("{}", s);
} // здесь s выходит из области видимости, память освобождается

Обратите внимание: никакого free() или delete. В конце блока компилятор неявно вставляет вызов деструктора — drop(). Это называется RAII (Resource Acquisition Is Initialization) — ресурс захватывается при инициализации и освобождается при разрушении объекта.

Владение (Ownership): главное правило Rust

Система владения в Rust строится на трёх простых правилах:

  1. У каждого значения в Rust есть ровно один владелец (owner).
  2. Когда владелец выходит из области видимости, значение удаляется.
  3. Владельца можно передать (move) или временно одолжить (borrow).

Звучит скучно? Давайте посмотрим, как это работает на практике.

Перемещение (Move)

fn main() {
    let s1 = String::from("hello");
    let s2 = s1; // s1 перемещается в s2
    // println!("{}", s1); // Ошибка! s1 больше не владеет данными
    println!("{}", s2); // OK
}

В C++ такое присваивание привело бы к двум указателям на одну память — и к двойному освобождению. В Rust компилятор просто запрещает использовать s1 после перемещения. Никаких утечек, никаких двойных удалений.

Копирование (Copy)

Простые типы, такие как целые числа, булевы значения и символы, реализуют трейт Copy. Для них присваивание создаёт полную копию:

fn main() {
    let x = 42;
    let y = x;
    println!("x = {}, y = {}", x, y); // Оба доступны!
}

Почему так? Потому что i32 хранится на стеке, и его копирование — это просто копирование нескольких байтов. Дешёво и безопасно.

Заимствование (Borrowing) и ссылки

Что делать, если нужно временно дать доступ к данным, не передавая владение? Использовать ссылки. Rust позволяет заимствовать значение по ссылке.

fn main() {
    let s = String::from("Rust");
    let len = calculate_length(&s); // передаём ссылку, а не владение
    println!("Длина '{}' равна {}", s, len); // s всё ещё доступна
}

fn calculate_length(s: &String) -> usize {
    s.len()
} // здесь ссылка s выходит из области видимости, но String не удаляется

Ссылки бывают двух видов:

  • Неизменяемые (&T) — можно читать, но нельзя изменять. Можно иметь сколько угодно таких ссылок.
  • Изменяемые (&mut T) — можно читать и изменять. Только одна такая ссылка может существовать в данный момент.

Это правило — гарантия отсутствия гонок данных (data races). Компилятор не позволит вам одновременно читать и писать в одну память из разных частей программы.

Пример: изменяемая ссылка

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
    println!("{}", s); // "hello, world"
}

fn change(s: &mut String) {
    s.push_str(", world");
}

Время жизни (Lifetimes): как Rust проверяет корректность ссылок

Теперь самое интересное. Компилятор должен убедиться, что ссылка не переживёт данные, на которые указывает. Для этого он использует механизм времён жизни (lifetimes).

В большинстве случаев Rust выводит времена жизни автоматически. Но иногда приходится указывать их явно — например, в функциях, которые возвращают ссылки.

fn longest(x: &str, y: &str) -> &str { // Ошибка! Нужно указать время жизни
    if x.len() > y.len() { x } else { y }
}

Компилятор не знает, какая из ссылок x или y будет возвращена, а значит, не может определить время жизни результата. Исправляем:

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

Здесь 'a — это параметр времени жизни. Он говорит: «Результат будет жить не дольше, чем оба аргумента». Компилятор теперь может проверить, что вызывающий код не использует возвращённую ссылку после того, как исходные данные удалены.

Сборщик мусора в Rust? Нет, RAII и умные указатели

В Rust нет ни GC, ни ручного управления памятью. Вместо этого используется RAII в сочетании с умными указателями (Box, Rc, Arc

Блоги

Рекомендаций по книгам