Философия Java

Как работает сборщик мусора


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

Например, вы можете думать о куче C++, как о загоне, в котором каждый объект содержится на своем участке площади. Это реальное положение позже было отброшено, и должно использоваться вновь. В некоторых JVM куча Java слегка отличается; она немного похожа на ленту конвейера, которая перемещается вперед всякий раз, когда вы резервируете новый объект. Это означает, что выделение хранилища для нового объекта происходит удивительно быстро. “Указатель кучи” просто перемещается вперед на не тронутую территорию, так что этот процесс по эффективности приближается к операциям со стеком в C++ (конечно в этом есть небольшой дополнительный расход на двойную бухгалтерию, но это ничто по сравнению с поиском хранилища).

Теперь вы можете заметить, что куча, фактически, не является лентой конвейера, и если вы трактуете ее таким образом, в конечном счете станете разбивать память на страницы (что советуется для увеличения производительности). Хитрость в том, что когда сборщик мусора выступает на сцену, и пока он собирает мусор, он компонует все объекты в куче так, что вы получаете реальное перемещение “указателя кучи” ближе к началу ленты конвейера, что предотвращает переполнение страницы. Сборщик мусора заново упорядочивает вещи и делает возможным использование модели высокоскоростной, постоянно свободной кучи для выделения хранилища.

Чтобы понять, как это работает, вам необходимо получить лучшее представление о различиях в схемах работы сборщиков мусора (СМ). Простая, но медленная техника СМ - это подсчет ссылок. Это означает, что каждый объект содержит счетчик ссылок, и при каждом присоединении ссылки к объекту, счетчик увеличивается. При каждом удалении ссылки из блока или установки ее в null, счетчик ссылок уменьшается. Таким образом, управление ссылками мало, но содержит накладные расходы, которые возникают на протяжении всего времени работы вашей программы. Сборщик мусора обходит весь список объектов, и когда находит объект, у которого счетчик ссылок равен нулю, освобождает это хранилище. Один из недостатков состоит в том, что если объект ссылается сам на себя, он может иметь не нулевой счетчик ссылок в то время, когда он может быть собран. Обнаружение таких самоссылающихся групп требует значительной дополнительной работы от сборщика мусора. Подсчет ссылок часто используется для объяснения одного из видов сбора мусора, но он не выглядит подходящим для реализации в любой JVM.


В быстрых схемах сбор мусора не основывается на подсчете ссылок. Вместо этого он основывается на идее того, что любой живой объект должен обязательно прослеживаться в обратном направлении к ссылке, которая расположена либо в стеке, либо в статическом хранилище. Цепочка может тянуться через несколько уровней объектов. Таким образом, если вы начнете в стеке и статическом хранилище, и пройдете по всем ссылкам, вы найдете все живые объекты. Для каждой ссылки, которую вы найдете, вы должны найти соответствующий объект, на который указывает эта ссылка, а затем проследить все ссылки этого объекта, далее проследить все ссылки тех объектов и т.д., пока вы не пройдете по всей паутине, тянущейся за ссылкой из стека или статического хранилища. Каждый объект, который вы просматриваете, должен быть живым. Обратите внимание, что нет проблем с определением самоссылающихся групп, они просто не будут найдены, и поэтому собираются автоматически.

В описанном здесь подходе JVM использует адаптивную схему сбора мусора, а то, что она делает с живыми объектами - это обнаруживает зависимости от вариантов использования. Один из этих вариантов - это остановись-и-копируй. Это означает, что — по некоторым причинам, которые вы увидите — программа сначала останавливается (эта схема не относится к фоновым). Затем, каждый найденный живой объект копируется из одной кучи в другую. То что осталось, собирается. Кроме того, когда объекты копируются в новую кучу, они упаковываются один к одному, таким образом, упаковывается новая куча (что позволяет быстро распутать новое хранилище до конца, как описано выше).

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



Есть две проблемы, которые делают этот так называемый “копирующий сборщик” не эффективным. Первая заключается в том, что вы имеете две кучи, и вы пересыпаете всю память вперед и назад между этими двумя различными кучами, занимая в два раза больше памяти, чем вам нужно на самом деле. Некоторые JVM делают это с помощью резервирования кучу частями, сколько нужно, а потом просто копируют из одного куска в другой.

Вторая проблема в копировании. Как только ваша программа стабилизируется, она будет генерировать мало мусора, или не генерировать его вообще. Несмотря на это копирующий сборщик будет копировать всю память из одного места в другое, что не экономично. Чтобы предотвратить это, некоторые JVM определяют, что не было сгенерировано нового мусора, и переключаются на другую схему (это “адаптивная” часть). Эта другая схема называется пометка и уборка. Эта схема была реализована в ранних версиях JVM от Sun и использовалась все время. Для общего использования, пометка и уборка достаточно медленна, но когда вы знаете, что генерируете мало мусора, или не генерируете его вообще, она быстра.

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

“Остановка-и-копирование” обращаются к идее того, что этот тип сборки мусора не выполняется в фоновом режиме; вместо этого программа останавливается, пока работает СМ. В литературе от Sun вы найдете много ссылок на сборку мусора, как на низкоприоритетный фоновый процесс, но это означает, что СМ не реализует этот способ, по крайней мере, в ранних версиях Sun JVM. Вместо этого сборщик мусора Sun запускается, когда памяти становится мало. Кроме того, пометка-и-уборка требует, чтобы программа остановилась.



Как упоминалось ранее, в описанной здесь JVM память резервируется большими блоками. Если вы резервируете большой объект, он получает свой собственный блок. Строгие правила остановки-и-копирования требуют копирования каждого живого объекта из исходной кучи в новую до того, как вы сможете освободить старую, что занимает много памяти. С блоками, СМ может обычно использовать мертвые блоки для копирования объектов, когда они будут собраны. Каждый блок имеет счет генерации, для слежения, жив ли он. В обычном случае создаются только блоки, так как СМ производит компактное расположение; все другие блоки получают увеличение своего счета генерации, указывающее, что на них есть ссылка откуда-либо. Это обработка обычного случая большинства временных объектов с коротким временем жизни. Периодически производится полная уборка — большие объекты все еще не копируются (просто получают увеличения счета генерации), а блоки, содержащие маленькие объекты, копируются и уплотняются. JVM следит за эффективностью СМ, и если она становится расточительной относительно времени, поскольку все объекты являются долгожителями, то она переключается на пометку-и-уборку. Аналогично, JVM следит, насколько успешна пометка-и-уборка, и, если куча становится слишком фрагментирована, вновь переключается на остановку-и-копирование. Это то, что составляет “адаптивную” часть, так что в завершении можно сказать труднопроизносимую фразу: “адаптивность генерирует остановку-и-копирование с пометкой-и-уборкой”.

Есть несколько дополнительный возможностей ускорения в JVM. Особенно важно включение операции загрузчика и Just-In-Time (JIT) компилятора. Когда класс должен быть загружен (обычно перед тем, как вы хотите создать объект этого класса), происходит поиск .class файла и байт-код для класса переносится в память. В этом месте один из подходов - упростит JIT весь код, но здесь есть два недостатка: это займет немного больше времени, что отразится на продолжительности работы программы, увеличив ее, и это увеличит размер выполнения (байт-код значительно компактнее, чем расширенный JIT-код), а это приведет к разбиению на страницы, которые значительно замедлят программу. Альтернативой является ленивое вычисление, что означает, что код не является JIT-компилированные, если это ненужно. Таким образом, код, который никогда не выполняется, никогда не будет компилироваться JIT.


Содержание раздела