Выбрать главу

Чтобы понять, как работает сборка мусора в Java, необходимо узнать, как устроены реализации сборщиков мусора (СМ) в других системах. Простой, но медленный механизм СМ называется подсчетом ссылок. С каждым объектом хранится счетчик ссылок на него, и всякий раз при присоединении новой ссыл¬ки к объекту этот счетчик увеличивается. Каждый раз при выходе ссылки из области действия или установке ее значения в null счетчик ссылок уменьша¬ется. Таким образом, подсчет ссылок создает небольшие, но постоянные из¬держки во время работы вашей программы. Сборщик мусора перебирает объект за объектом списка; обнаружив объект с нулевым счетчиком, он освобождает ресурсы, занимаемые этим объектом. Но существует одна проблема — если объ¬екты содержат циклические ссылки друг на друга, их счетчики ссылок не обну¬ляются, хотя на самом деле объекты уже являются «мусором». Обнаружение таких «циклических» групп является серьезной работой и отнимает у сборщика мусора достаточно времени. Подсчет ссылок часто используется для объясне¬ния принципов процесса сборки мусора, но, судя по всему, он не используется ни в одной из виртуальных машин Java.

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

В описанном здесь подходе работает адаптивный механизм сбора мусора, при котором JVM обращается с найденными используемыми объектами соглас¬но определенному варианту действий. Один из таких вариантов называется ос- тановить-и-копировать. Смысл термина понятен: работа программы временно приостанавливается (эта схема не поддерживает сборку мусора в фоновом ре¬жиме). Затем все найденные «живые» (используемые) объекты копируются из одной кучи в другую, а «мусор» остается в первой. При копировании объек¬тов в новую кучу они размещаются в виде компактной непрерывной цепочки, высвобождая пространство в куче {и позволяя удовлетворять заказ на новое хранилище простым перемещением указателя).

Конечно, когда объект перемещается из одного места в другое, все ссылки, указывающие на него, должны быть изменены. Ссылки в стеке или в статиче¬ском хранилище переопределяются сразу, но могут быть и другие ссылки на этот объект, которые исправляются позже, во время очередного «прохода». Исправление происходит по мере нахождения ссылок.

Существует два фактора, из-за которых «копирующие сборщики» обладают низкой эффективностью. Во-первых, в системе существует две кучи, и вы «пе¬релопачиваете» память то туда, то сюда между двумя отдельными кучами, при этом половина памяти тратится впустую. Некоторые JVM пытаются решить эту проблему, выделяя память для кучи небольшими порциями по мере необхо¬димости, а затем просто копируя одну порцию в другую.

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

Схема «пометить-и-убрать» использует ту же логику — проверка начинается со стека и статического хранилища, после чего постепенно обнаруживаются все ссылки на «живые» объекты. Однако каждый раз при нахождении объект поме¬чается флагом, но еще продолжает существование. «Уборка» происходит только после завершения процесса проверки и пометки. Все «мертвые» объекты при этом удаляются. Но копирования не происходит, и если сборщик решит «упа¬ковать» фрагментированную кучу, то делается это перемещением объектов внут¬ри нее.

Идея «остановиться-и-копировать» несовместима с фоновым процессом сборки мусора; в начале уборки программа останавливается. В литературе фир¬мы Sun можно найти немало заявлений о том, что сборка мусора является фо¬новым процессом с низким приоритетом, но оказывается, что реализации в та¬ком виде (по крайней мере в первых реализациях виртуальной машины Sun) в действительности не существует. Вместо этого сборщик мусора от Sun начи¬нал выполнение только при нехватке памяти. Схема «пометить-и-убрать» так¬же требует остановки программы.

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

Существуют и другие способы ускорения работы в JVM. Наиболее важ¬ные — это действия загрузчика и то, что называется компиляцией «на лету» (Just-In-Time, JIT). Компилятор JIT частично или полностью конвертирует программу в «родной» машинный код, благодаря чему последний не нуждается в обработке виртуальной машиной и может выполняться гораздо быстрее. При загрузке класса (обычно это происходит при первом создании объекта этого класса) система находит файл .class, и байт-код из этого файла переносится в память. В этот момент можно просто провести компиляцию JIT для кода класса, но такой подход имеет два недостатка: во-первых, это займет чуть больше времени, что вместе с жизненным циклом программы может серьезно отразиться на производительности. Во-вторых, увеличивается размер исполняемого файла (байт-код занимает гораздо меньше места в сравнении с расширенным кодом JIT), что может привести к подкачке памяти, и это тоже замедлит программу. Альтернативная схема отложенного вычисления подразумевает, что код JIT компилируется только тогда, когда это станет необходимо. Иначе говоря, код, который никогда не исполняется, не компилируется JIT. Новая технология Java HotSpot, встроенная в последние версии JDK, делает это похожим образом с применением последовательной оптимизации кода при каждом его выполне¬нии. Таким образом, чем чаще выполняется код, тем быстрее он работает.