1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/wizardforcel-thinking-in-java-zh

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
В этом репозитории не указан файл с открытой лицензией (LICENSE). При использовании обратитесь к конкретному описанию проекта и его зависимостям в коде.
Клонировать/Скачать
附录E 关于垃圾收集的一些话.md 23 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 11.03.2025 09:15 d56454c

Приложение Е Несколько слов о сборке мусора

«Необычно, что Java может быть такой же быстрой, как C++, а иногда даже быстрее».

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

Моё внимание к скорости частично вызвано моделью C++. C++ сосредоточен на всех «статических» вещах, происходящих во время компиляции, поэтому версия программы в момент выполнения очень короткая и быстрая. C++ также основан на модели C (в основном для обратной совместимости), но иногда просто потому, что он работает определенным образом в C, это становится самым удобным способом в C++. Самым важным случаем является управление памятью в C и C++, которое является основанием для мнения некоторых людей о том, что Java должна быть медленнее: во всех объектах Java требуется создание в памяти «стека».В то время как в C++ объекты создаются в стеке. Это позволяет достичь большей скорости, поскольку при входе в определённую область видимости указатель стека перемещается на одну единицу вниз, выделяя место для всех объектов, созданных в этой области видимости и использующих стек. Когда мы покидаем область видимости (после завершения всех локальных конструкторов), указатель стека перемещается на одну единицу вверх. Однако создание объектов в «куче» (heap) в C++ обычно происходит гораздо медленнее, так как оно основано на модели управления памятью C. Этот стек фактически представляет собой большой буфер памяти, который требует рециркуляции (повторного использования). После вызова delete в C++ освобожденная память остаётся в виде отверстий в куче, поэтому при следующем вызове new механизм выделения памяти должен провести некоторый поиск, чтобы найти подходящее место для нового объекта среди существующих отверстий. В противном случае быстро закончится доступная память в куче. Поэтому создание объектов в стеке оказывает значительное влияние на производительность из-за необходимости поиска свободной памяти. Таким образом, создание объектов в стеке происходит значительно быстрее. Аналогично тому, что так много работы в C++ выполняется во время компиляции, следует принимать это во внимание.Однако в некоторых аспектах Java события происходят значительно "динамичнее", что приводит к изменениям модели. При создании объектов использование сборщика мусора оказывает значительное влияние на скорость создания объектов. На первый взгляд это может показаться странным — освобождение памяти влияет на выделение памяти, но это один из важнейших методов JVM, который позволяет выделять память для объектов в куче практически так же быстро, как создаются объекты на стеке в C++. Представьте себе C++ кучу (а также более медленную Java кучу) как двор, где каждый объект имеет свою землю. В будущем это "недвижимое имущество" будет выброшено, и его обязательно придется переиспользовать. Однако в некоторых JVM Java куча работает совершенно по-другому. Она больше похожа на конвейер: после каждого нового объекта выделение памяти происходит вперед. Это означает, что выделение пространства для хранения объектов может происходить с очень высокой скоростью. "Конвейерный указатель" просто перемещается вперед до свободной области, поэтому этот процесс почти полностью совпадает с выделением памяти на стеке в C++. (Конечно, здесь требуется больше затрат на запись данных, но это намного быстрее поиска свободного места.)Теперь обратите внимание, что реальная куча не является конвейером. Если бы мы использовали её таким образом, то рано или поздно потребовалось бы большое количество страниц (что существенно влияло бы на производительность), а затем закончилась бы память, и возникли бы ошибки страницирования. Поэтому здесь используется трюк, известный как "мусорный сборщик". Он собирает мусор и одновременно компактизирует все объекты в куче, перемещая "конвейерный указатель" как можно ближе к началу конвейера, тем самым отдалённый от мест, где могут возникнуть ошибки страницирования. Мусорный сборщик перестраивает всё так, чтобы создать высокопроизводительную и бесконечную модель кучи, при этом эффективно распределяя память.Чтобы действительно понять принцип работы, нам сначала нужно понять различные подходы, используемые различными мусорными сборщиками (GC). Одним из самых простых, но менее быстрых методов GC является счетчик ссылок. Это значит, что каждый объект содержит счетчик ссылок. Каждый раз, когда новая ссылка связана с одним и тем же объектом, счетчик ссылок увеличивается. Когда ссылка выходит за пределы своего действия или устанавливается равной null, счетчик ссылок уменьшается. Таким образом, пока программа выполняется, постоянно требуется управление счетчиками ссылок — хотя сами затраты управления сравнительно небольшие. Мусорный сборщик проходит через весь список объектов, и если он обнаруживает, что один из счетчиков ссылок стал равен 0, он освобождает память, занятую этим объектом. Но есть и недостаток этого подхода: если объекты циклически связаны друг с другом, даже если счетчики ссылок не равны 0, они могут считаться мусором, который следует удалить. Для того чтобы найти такие самочинные группы, мусорному сборщику требуются значительные дополнительные усилия. Счетчик ссылок представляет собой тип мусорного сбора, но он кажется не слишком подходящим для всех сценариев использования JVM. В более быстром подходе сборка мусора не основана на счёте ссылок.Вместо этого она базируется на принципе, что все живые объекты в конечном итоге могут быть отслежены до какой-либо ссылки, которая либо находится в стеке, либо в статическом пространстве хранения. Эта цепочка отслеживания может пройти через несколько уровней объектов. Таким образом, если начать со стека и статического пространства хранения и последовать за всеми ссылками, можно найти всех активных объектов. Для каждого найденного объекта следует следовать за каждой ссылкой, которую он указывает, затем следовать за ссылками внутри этих объектов... и так далее, пока вся цепь ссылок, начинающаяся со стека или статического пространства хранения, не будет полностью проследована. Каждый объект, который встречается по пути, должен быть активным. Обратите внимание, что для специальных групп самочитающих объектов вышеупомянутые проблемы не возникают. Поскольку они просто недостижимы, они автоматически считаются мусором.Метод, описанный здесь, использует "адаптивную" систему сборки мусора JVM. Конкретные действия, применяемые к каждому найденному активному объекту, зависят от того, какую версию системы сборки мусора используют в данный момент. Одной из таких версий является "стоп-и-копирование". Это значит, что программа временно прекращает свою работу (не являясь фоновой системой сборки мусора). Затем каждый найденный активный объект копируется из одной кучи памяти в другую, оставляя всё остальное как мусор. Кроме того, поскольку объекты копируются в новую кучу, они становятся компактными (что позволяет легко выделять новые области хранения путём удаления старых областей, как было описано ранее).

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

Второй вопрос связан с копированием. По мере того как программы становятся всё более "жёсткими", они почти не производят или производят очень мало мусора. Тем не менее, сборщик мусора продолжает копировать всю память с одного места на другое, что выглядит крайне нерационально. Чтобы решить эту проблему, некоторые JVM могут обнаруживать отсутствие нового мусора и немедленно переходить к альтернативному подходу (это и есть причина "адаптивности"). Альтернативный подход называется "маркировка и очистка", и именно этот метод использовался в JVM Sun.Для типичных приложений маркировка и очистка кажутся очень медленными, но если программа знает, что она не создаёт мусор или создаёт его в минимальных количествах, её производительность значительно возрастает.

Маркировка и очистка используют ту же логику: они начинают с стека и статических данных и следуют за всеми ссылками, чтобы найти активные объекты. Однако каждый раз, когда находится активный объект, ему проставляется метка, то есть делается отметка об этом объекте. В это время сборка мусора ещё не происходит. Лишь после завершения процесса маркировки начинается процесс очистки. Во время очистки заблокированные объекты освобождаются, однако никакого копирования не происходит, поэтому если сборщик мусора решил компактно организовать непрерывную область памяти, он перемещает окружающие объекты."Stop-and-copy" показывает, что такой тип сборки мусора не выполняется фоново; вместо этого, как только начинается сборка мусора, программа прекращает свою работу. В документации Sun можно найти множество мест, где сборка мусора определяется как низкоприоритетный фоновый процесс, но это лишь теоретический эксперимент, который на практике не работает. На практике сборщики мусора Sun запускаются, когда память начинает заканчиваться. Кроме того, "mark-and-sweep" также требует, чтобы программа была остановлена во время выполнения. Как было отмечено ранее, в данном JVM память выделяется крупными блоками. При выделении одного такого большого блока объект получает свой блок памяти. Строгий "stop-and-copy" требует перемещения каждого активного объекта из исходной кучи в новую перед очисткой старой кучи, что приводит к значительному объему работы по переносу данных. Блоки памяти позволяют сборщику мусора использовать "мертвые" блоки для копирования объектов так же, как он это делает при сборке мусора. Каждый блок имеет счетчик генераций, который используется для отслеживания того, остается ли он "живым". Обычно только те блоки, которые были созданы с момента последней сборки мусора, будут сжаты; для всех других блоков счетчики генераций будут переполнены, если они все еще ссылаются из другого места.Это типично для короткоживущих временных объектов. Полная очистка происходит периодически — большие блоки остаются незакопированными (лишь счетчики генераций переполняются), а блоки, содержащие маленькие объекты, будут скопированы и сжаты. JVM следит за эффективностью сборщика мусора и переключается на "mark-and-sweep", если все объекты являются долгоживущими и сборка мусора становится бесполезной затратой времени. Аналогичным образом, JVM отслеживает успешные "mark-and-sweep" операции и возвращает к "stop-and-copy", когда куча начинает становиться всё более "разбросанной". "Пользовательская" характеристика этого поведения заключается в том, что мы можем свести её к следующему: "Автоматическое переключение между режимами stop-and-copy/mark-and-sweep в зависимости от ситуации". JVM также использует множество других ускоряющих схем. Одна из особенно важных связана с загрузчиками и JIT-компилятором. При необходимости загрузки класса (обычно это происходит при первом создании объекта этого класса) ищется файл .class, а байт-код данного класса помещается в память.В данный момент можно было бы JIT-скомпилировать весь код, но такой подход имеет два недостатка: он занимает больше времени, причём время компиляции может оказаться даже большим, чем общее время выполнения программы; и увеличивает размер исполняемых файлов (байт-код значительно меньше расширенного JIT-кода), что может привести к страницированию памяти и значительному замедлению работы программы. Вместо этого можно выбрать альтернативный подход: JIT-скомпилировать код только тогда, когда это действительно необходимо. Таким образом, код, который никогда не будет выполнен, никогда не будет JIT-скомпилирован.Поскольку JVM является внешним компонентом для браузера, возможно, вы захотите воспользоваться возможностью увеличения скорости JVM во время использования браузера. Однако очень жаль, что в настоящее время JVM не может взаимодействовать с различными браузерами. Чтобы использовать весь потенциал конкретной JVM, вам придётся использовать браузер, встроенный с этой JVM, либо запустить независимое Java-приложение.

Опубликовать ( 0 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/wizardforcel-thinking-in-java-zh.git
git@api.gitlife.ru:oschina-mirror/wizardforcel-thinking-in-java-zh.git
oschina-mirror
wizardforcel-thinking-in-java-zh
wizardforcel-thinking-in-java-zh
master