Свободный и независимый вомбат думает о памяти

Хотел написать про работу системы управления памятью, но понял, что для полноценной статьи у меня не хватает практических знаний. Поэтому кратко

Куча

Баян
Баян

Помимо стека, о котором было рассказано в прошлой статье и который содержит локальные переменные, в "потребительской" программе в среднестатистической ОС еще бывают глобальные переменные и динамическая память. Глобальные переменные описываются внутри сегментов .bss и .data, резервируясь в памяти сразу после загрузки образа программы (по-правильному это называется процессом).

Динамическая память иначе называется кучей, так как представляет из себя кучу динамических данных, который могут появляться и исчезать во время работы.

Для работы с динамической памятью в C существует malloc(size_t), запрашивающий (выделяющий, аллоцирующий) у ОС кусок памяти указанного размера и возвращающий указатель на первый байт, и free(void *), принимающий этот указатель и возвращающий (освобождающий) кусок назад в ОС.

В C++ есть операторы new и delete, они устроены гораздо сложнее и я не буду их рассматривать, ибо не пишу на нем. Помимо запроса памяти у ОС они занимаются ее инициализацией.

Если по какой-то причине стандартной библиотеки языка в наличии не имеется, то в Windows за память отвечает HeapAlloc и устаревшие GlobalAlloc и LocalAlloc из времен Windows 3.11 (malloc к ним и обращается внутри. Еще есть парные HeapFree, GlobalFree и LocalFree). Они предоставляют больше контроля над результатом, но проприетарны для Windows. А полный список функций для работы с памятью в WinAPI есть здесь, их сотни на любой случай жизни. Можно даже внутри контекста чужого процесса выделить или освободить кусок памяти (обычно такое используется вирусами и прочей нечистью).

Утечки

А что будет, если постоянно выделять, но не освобождать? А еще лучше: выделять, но терять адреса выделенной памяти. ОС достаточно быстро подскажет правильный ответ.

В теории есть несколько возможных выходов, можно даже обработать подобный случай и, к примеру, экстренно освободить всё выделенное (только если не терять указатели) или попытаться спасти нужные данные из разваливающейся программы, но если никаких действий не предпринять, то ОС заставит процесс экстренно прекратить свою деятельность и существование.

Случай с потерей указателей на выделенную память называется утечкой памяти и ее последствия необратимы. Но как только ОС завершает выполнение процесса, то вся его память, даже утекшая, становится снова свободной.

Свободный и независимый вомбат думает о памяти

Ленивые программисты и мусоросборка

Отслеживание правильности выделения и освобождения памяти является достаточно сложной и опасной задачей, особенно если требуется обеспечивать стабильность и безопасность (лучше даже не думать, что будет, если рукожопый программист забьет на MISRA и устроит утечку памяти в подсистеме тормозов в Тесле), и практически невыполнимой в некоторых парадигмах (особенно в функциональном программировании). Поэтому еще с древнейших времен (с 1959 года) существуют языки со сборкой мусора:

Особенности

  • Первое: в таких языках указателей или нет вообще, или они очень ограничены в пользовании (C#).
  • Второе: для работы с динамической памятью вместо указателей применяются ссылки. Ссылка по факту представляет из себя тот же указатель, только очень сильно ограниченный. Над ссылками запрещены все операции, кроме присваивания значения другой ссылки и разыменования, в то время как указатели имеют права и возможности обычных целых чисел.
  • Третье: Каждый выделенный кусок памяти всегда занят каким-то объектом.
  • Четвертое: такие языки всегда тянут за собой подсистему сборки мусора, которая самостоятельно учитывает каждый объект и выделенный под него кусок памяти, а еще подсчитывает количество активных ссылок на каждый подчиненный объект. Это называется подсчетом ссылок.
  • Пятое: как только процент использованной памяти переходит через порог, то запускается сборщик мусора, который сканирует все объекты и количество существующих на них ссылок. Если ссылок на объект нет (то есть он "утек"), то его память освобождается. 

Минусы и проблемы

  • Дорого. При злоупотреблении сборщик мусора может запускаться слишком часто, а каждый его запуск вносит заметный лаг. Особенно критично это в играх.
  • Он не устраняет все возможные способы вызвать утечку памяти, к примеру просто выделяя бессмысленные данные, но сохраняя на них ссылки. Или если объект, который управляется сборщиком мусора будет содержать в себе то, что сборщиком мусора не управляется (открытые файлы или нативная память вне его подчинения).
  • А еще он изредка может удалять то, что удалять не нужно, поэтому появляются костыли уровня GC.KeepAlive() (а-ля самый маленький метод в C#).

Но несмотря не это, практически все современные языки используют сборку мусора, кроме C++ и Rust. C++ по большей части перешел на умные указатели, которые тоже считают количество активных ссылок, но делают это без сборщика мусора. А Rust использует свой уникальный Borrow Checker, который понимают только растофилы.

На этом вроде у меня всё.