За что мы любим языки с автоматической сборкой мусора ? За то, что нам с вами приходится меньше думать. Мы просто создаем новые объекты, а когда они оказываются ненужны, RTS сама освобождает память. Проблема утечки памяти решена, жизнь прекрасна и удивительна! Вот только, к сожалению, это неправда.
Автоматическая у вас сборка мусора или нет, память в ваших программах все равно течет.
Java. Рассмотрим замечательный пример со стеком из книги Effective Java. Можете ли вы обнаружить в нем утечку памяти? Не спешите читать дальше, посмотрите на пример и подумайте… Я серьезно , это очень интересное упражнение. Ладно, рассказываю. Поскольку ссылки в массиве elements не обнуляются в методе pop, объекты, однажды помещенные в стек, не будут освобождены до тех пор, пока стек не будет уничтожен. Что намного хуже, объекты, на которые ссылаются объекты, однажды помещенные в стек, также будут висеть в памяти, пока существует стек. Или пока ссылки в стеке не будут перезаписаны другими ссылками.
Go и другие. Течет по той же причине, по которой течет и Java. Вообще, пример со стеком применим к любому языку с автоматической сборкой мусора , если в нем есть изменяемые переменные.
Python. Создадим два объекта, имеющих деструкторы. Сошлемся из первого объекта на второй, а из второго на первый. Перестанем использовать объекты. Вопрос — какой объект будет уничтожен первым? Допустим, первый. Но что, если второй объект использует первый в своем деструкторе? Все сломается! Поэтому в такой ситуации Python не освобождает память. Оба объекта будут висеть в памяти до тех пор, пока работает программа. А если таких цепочек объектов создается много, программа быстро упадет с out of memory.
Erlang. Известен своей способностью создавать мемори лики при работе с большими кусками бинарных данных. Допустим, есть бинарь размером 1 Мб. Нам нужны первые 3 байта из него. Мы можем легко получить их при помощи сопоставления с образцом. Но физически это те же первые три байта из 1 Мб данных. Даже если весь бинарь целиком не используется, выделенные 1 Мб памяти не могут быть освобождены. Решить эту проблему можно, используя фукнцию binary:copy() . Однако бездумно копировать все и вся не только дорого, но и может приводить к еще большим мемори ликам, если оригинальные 1 Мб все-таки используются! А еще в Erlang постоянно переполняются очереди .
Haskell. Все бояться хаскеля из-за ленивых вычислений . И, возможно, не зря. Допустим, мы написали рекурсивную функцию с аргументом-аккумулятором. Будучи ленивым языком, Haskell будет постоянно откладывать вычисление этого аргумента. Вместо вычисления, язык будет выделять все больше и больше памяти, содержащей информацию о том, как произвести эти вычисления. Анализ строгости (strictness analysis) отчасти решает эту проблему. Но факт остается фактом, при использовании ленивых вычислений приходится дополнительно думать о том, чтобы память не утекла.
Как видите, не важно, на императивном языке вы пишите или функциональном, строгом или ленивом, память при использовании GC все равно утекает. А раз так, может быть вообще отказаться от использования GC? Например, давайте использовать в программе умные указатели (которые со счетчиками ссылок), а для особых случаев, вроде создания двусвязных списков, обычные указатели.
Мы сразу лишаемся такого количества проблем! Никаких накладных расходов на GC, никаких stop the world ! Программа работает быстро, словно если бы мы писали на C++, память расходуется почти так же экономно, как если бы мы выделяли ее вручную! Кроме того, в нашем арсенале появляются такие приемы, как «а давайте уместим наши данные в L1 кэш».
Чем за это приходится платить? Если у нас появятся циклические ссылки, память утечет. Но так она может утечь и при использовании полноценного GC! Так что тут нет особой разницы. Вот проблема фрагментации памяти куда актуальнее. Но давайте подумаем, а многие ли из нас сталкивались с этой проблемой на практике? Лично я, кажется, еще ни разу. А ведь мы ежедневно используем кучу программ, написанных на C/C++. Ну и потом, хоть я и не специалист в этом вопросе, сдается мне, что проблема фрагментации памяти сравнительно легко решается при помощи повторного использования объектов, приемов вроде slab allocator’ов и так далее. Если вдруг вы с ней столкнетесь.
В общем, идея написания в третьем тысячелетии программ без использования полноценного GC, как предлагается в языках вроде Rust или Vala , не кажется такой уж безумной.
Дополнение: Некто Index Int подсказал в комментариях ссылку на отличную статью по теме. В частности, в ней объяснено, почему молодые объекты выгоднее собирать с помощью GC, а старые — при помощи счетчиков ссылок. Следует однако отметить, что в Rust молодые объекты размещаются на стеке и быстро освобождаются.