Введение
Методология управления памятью компьютера
В информатике ручное управление памятью относится к использованию программистом непосредственных инструкций для выявления и освобождения неиспользуемых объектов, или мусора. До середины 1990-х годов большинство языков программирования, используемых в промышленности, поддерживали ручное управление памятью, хотя сборка мусора существовала с 1959 года, когда она была представлена вместе с Lisp. Однако сегодня всё большую популярность приобретают языки со сборкой мусора, такие как Java, а языки Objective-C и Swift предоставляют аналогичную функциональность посредством автоматического подсчёта ссылок. Основными языками с ручным управлением памятью, которые до сих пор широко используются, являются C и C++ – см. Динамическое выделение памяти в C.
Описание
Многие языки программирования используют ручные методы для определения момента выделения нового объекта из свободной памяти. В C используется функция `malloc`; в C++ и Java – оператор `new`; а многие другие языки (например, Python) выделяют все объекты из свободной памяти. Определение момента создания объекта (создание объекта) обычно тривиально и не вызывает проблем, хотя такие методы, как пулы объектов, могут приводить к созданию объекта до его непосредственного использования. Настоящая сложность заключается в уничтожении объекта – определении момента, когда объект больше не нужен (то есть является мусором), и организации возврата занимаемой им памяти в свободную память для повторного использования. При ручном управлении памятью это также определяется программистом вручную, с помощью функций, таких как `free` в C или оператора `delete` в C++. Это отличается от автоматического уничтожения объектов, хранящихся в автоматических переменных, в частности (нестатических) локальных переменных функций, которые уничтожаются в конце области их видимости в C и C++.
Ручное управление и правильность
Известно, что неправильное использование ручного управления памятью может привести к нескольким основным классам ошибок в программе, в частности к нарушениям безопасности памяти или утечкам памяти. Это существенный источник уязвимостей в безопасности. Утечка памяти возникает, когда неиспользуемый объект никогда не возвращается в пул свободной памяти. В некоторых случаях утечки памяти могут быть допустимы, например, если программа "утекает" ограниченный объем памяти в течение всего времени работы, или если это программа короткого действия, которая полагается на операционную систему для освобождения ресурсов при завершении. Однако во многих случаях утечки памяти происходят в долгоживущих программах, и в таких случаях объем утекающей памяти не ограничен. Когда это происходит, размер доступного пула свободной памяти постепенно уменьшается; когда пул полностью исчерпан, программа аварийно завершает работу. Катастрофический сбой системы динамического управления памятью может произойти, если память, выделенная под объект, будет освобождена несколько раз; объект будет явно уничтожен несколько раз; если, используя указатель для работы с объектом, не выделенным в пуле свободной памяти, программист попытается освободить память, занимаемую этим объектом; или если, манипулируя объектом через указатель на другую, произвольную область памяти, управляемую неизвестной внешней задачей, потоком или процессом, программист повредит состояние этого объекта, возможно, даже выйдет за его границы и повредит данные управления памятью. Результатом таких действий может быть повреждение кучи, преждевременное уничтожение другого (и недавно созданного) объекта, который случайно занимает то же место в памяти, что и многократно удаленный объект, сбой программы из-за ошибки сегментации (нарушения защиты памяти) и другие формы неопределенного поведения. Указатели на удаленные объекты становятся "висячими" (wild pointers), если их использовать после освобождения памяти; попытка использовать такие указатели может привести к труднодиагностируемым ошибкам. Языки, использующие исключительно сборку мусора, позволяют избежать двух последних классов дефектов. Утечки памяти все еще могут возникать (и ограниченные утечки часто встречаются при использовании генерационной или консервативной сборки мусора), но, как правило, они менее серьезны, чем утечки памяти в системах с ручным управлением.
Приобретение ресурсов - это инициализация
Ручное управление памятью имеет одно преимущество с точки зрения корректности: оно позволяет автоматическое управление ресурсами посредством парадигмы "Приобретение ресурса – это инициализация" (RAII). Это особенно полезно, когда объекты владеют дефицитными системными ресурсами (например, графическими ресурсами, дескрипторами файлов или соединениями с базами данных), которые необходимо освободить при уничтожении объекта – когда время жизни ресурса должно быть привязано к времени жизни объекта. Языки с ручным управлением могут реализовать это, приобретая ресурс во время инициализации объекта (в конструкторе) и освобождая его во время уничтожения объекта (в деструкторе), что происходит в точно определенный момент. Это и есть парадигма "Приобретение ресурса – это инициализация". Она также может использоваться с детерминированным подсчетом ссылок. В C++ эта возможность используется для автоматизации освобождения памяти в рамках ручной системы управления памятью; использование шаблона `shared_ptr` из стандартной библиотеки языка для управления памятью является распространенной практикой. Однако `shared_ptr` не подходит для всех сценариев использования объектов. Этот подход неприменим в большинстве языков со сборкой мусора – в частности, в системах трассирующей сборки мусора или более продвинутых реализациях подсчета ссылок – из-за недетерминированности финализации и, иногда, её полного отсутствия. То есть, сложно определить (или гарантировать), когда и будет ли вызван метод финализации; эта проблема известна как "проблема финализатора". Java и другие языки со сборкой мусора часто используют ручное управление для дефицитных системных ресурсов, помимо памяти, посредством шаблона "dispose": любой объект, управляющий ресурсами, должен реализовывать метод `dispose`, который освобождает эти ресурсы и помечает объект как неактивный. Программисты должны вызывать `dispose` вручную, когда это необходимо, чтобы предотвратить "утечку" дефицитных графических ресурсов. Использование метода `finalize` (реализация финализаторов в Java) для освобождения графических ресурсов широко считается плохой практикой среди программистов Java, и аналогично, метод `del` в Python не может быть надежным способом освобождения ресурсов. Для ресурсов стека (ресурсов, приобретаемых и освобождаемых в пределах одного блока кода) автоматизация возможна с помощью различных языковых конструкций, таких как `with` в Python, `using` в C# или `try-with-resources` в Java.