Автор оригинала: Attila Fejér.
1. введение
Сегодня нередки случаи, когда приложения одновременно обслуживают тысячи или даже миллионы пользователей. Такие приложения требуют огромного объема памяти. Однако управление всей этой памятью может легко повлиять на производительность приложения.
Чтобы решить эту проблему, Java 11 представила сборщик мусора Z (ZGC) в качестве экспериментальной реализации сборщика мусора (GC).
В этом уроке мы увидим как ZGC удается поддерживать низкое время паузы даже на многотерабайтных кучах .
2. Основные Понятия
Чтобы понять, как работает ZGC, нам нужно понять основные концепции и терминологию, лежащие в основе управления памятью и сборщиков мусора .
2.1. Управление памятью
Физическая память-это оперативная память, которую обеспечивает наше оборудование.
Операционная система (ОС) выделяет пространство виртуальной памяти для каждого приложения.
Конечно, мы храним виртуальную память в физической памяти, и ОС отвечает за поддержание сопоставления между ними. Это сопоставление обычно включает аппаратное ускорение.
2.2. Множественное отображение
Множественное отображение означает, что в виртуальной памяти есть определенные адреса, которые указывают на один и тот же адрес в физической памяти. Поскольку приложения получают доступ к данным через виртуальную память, они ничего не знают об этом механизме (и им это не нужно).
По сути, мы сопоставляем несколько диапазонов виртуальной памяти с одним и тем же диапазоном в физической памяти:
На первый взгляд, его варианты использования не очевидны, но позже мы увидим, что ZGC нуждается в нем, чтобы творить свою магию. Кроме того, он обеспечивает некоторую безопасность, поскольку разделяет области памяти приложений.
2.3. Переезд
Поскольку мы используем динамическое распределение памяти, память среднего приложения со временем становится фрагментированной. Это потому, что, когда мы освобождаем объект в середине памяти, там остается пробел свободного пространства. Со временем эти пробелы накапливаются, и наша память будет выглядеть как шахматная доска, состоящая из чередующихся областей свободного и используемого пространства.
Конечно, мы могли бы попытаться заполнить эти пробелы новыми объектами. Чтобы сделать это, мы должны просканировать память на наличие свободного места, достаточно большого, чтобы вместить наш объект. Это дорогостоящая операция, особенно если нам приходится делать это каждый раз, когда мы хотим выделить память. Кроме того, память все равно будет фрагментирована, так как, вероятно, мы не сможем найти свободное пространство, которое имеет точный размер, который нам нужен. Поэтому между объектами будут промежутки. Конечно, эти пробелы меньше. Кроме того, мы можем попытаться минимизировать эти пробелы, но для этого требуется еще больше вычислительной мощности.
Другая стратегия заключается в частом перемещении объектов из фрагментированных областей памяти в свободные области в более компактном формате . Чтобы быть более эффективными, мы разделяем пространство памяти на блоки. Мы перемещаем все объекты в блоке или ни один из них. Таким образом, выделение памяти будет происходить быстрее, так как мы знаем, что в памяти есть целые пустые блоки.
2.4. Сбор мусора
Когда мы создаем Java-приложение, нам не нужно освобождать выделенную память, потому что сборщики мусора делают это за нас. Таким образом, GC отслеживает, до каких объектов мы можем добраться из нашего приложения через цепочку ссылок, и освобождает те, до которых мы не можем добраться .
GC должен отслеживать состояние объектов в пространстве кучи, чтобы выполнять свою работу. Например, возможное состояние достижимо. Это означает, что приложение содержит ссылку на объект. Эта ссылка может быть транзитивной. Единственное, что имеет значение, что приложение может получить доступ к этим объектам через ссылки. Другой пример-финализуемый: объекты, к которым мы не можем получить доступ. Это объекты, которые мы считаем мусором.
Для достижения этой цели сборщики мусора имеют несколько этапов.
2.5. Свойства фазы ГК
Фазы ГК могут иметь различные свойства:
- параллельная фаза может выполняться в нескольких потоках GC
- последовательная фаза выполняется в одном потоке
- a stop-the-world фаза не может выполняться одновременно с кодом приложения
- фаза concurrent может выполняться в фоновом режиме, в то время как наше приложение выполняет свою работу
- инкрементная фаза может завершиться до завершения всей своей работы и продолжить ее позже
Обратите внимание, что все вышеперечисленные методы имеют свои сильные и слабые стороны. Например, предположим, что у нас есть фаза, которая может выполняться одновременно с нашим приложением. Последовательная реализация этой фазы требует 1% от общей производительности процессора и выполняется в течение 1000 мс. В отличие от этого, параллельная реализация использует 30% процессора и завершает свою работу за 50 мс.
В этом примере параллельное решение использует больше ЦП в целом, поскольку оно может быть более сложным и требует синхронизации потоков . Для приложений с тяжелым процессором (например, пакетные задания) это проблема, поскольку у нас меньше вычислительной мощности для выполнения полезной работы.
Конечно, в этом примере есть выдуманные цифры. Однако ясно, что все приложения имеют свои характеристики, поэтому у них разные требования к GC.
Для получения более подробных описаний, пожалуйста, посетите нашу статью об управлении памятью Java .
3. Концепции ZGC
ZGC намерена обеспечить этапы “останови мир” как можно короче. Это достигается таким образом, что продолжительность этих пауз не увеличивается с размером кучи. Эти характеристики делают ZGC хорошо подходящим для серверных приложений, где часто встречаются большие кучи и требуется быстрое время отклика приложений.
В дополнение к проверенным и проверенным методам GC, GC вводит новые концепции, которые мы рассмотрим в следующих разделах.
Но сейчас давайте взглянем на общую картину того, как работает ZGC.
3.1. Общая картина
У ZGC есть фаза, называемая маркировкой, где мы находим достижимые объекты. GC может хранить информацию о состоянии объекта несколькими способами. Например, мы могли бы создать карту , где ключи-это адреса памяти, а значение-состояние объекта по этому адресу. Это просто, но для хранения этой информации требуется дополнительная память. Кроме того, поддержание такой карты может быть сложной задачей.
ZGC использует другой подход: он сохраняет состояние ссылки в виде битов ссылки. Это называется эталонной раскраской. Но таким образом перед нами встает новая задача. Установка битов ссылки для хранения метаданных об объекте означает, что несколько ссылок могут указывать на один и тот же объект, поскольку биты состояния не содержат никакой информации о местоположении объекта. Мульти-отображение на помощь!
Мы также хотим уменьшить фрагментацию памяти. Для этого ZGC использует перемещение. Но с большой кучей перемещение-это медленный процесс. Поскольку ZGC не хочет длительных пауз, он выполняет большую часть перемещения параллельно с приложением. Но это создает новую проблему.
Допустим, у нас есть ссылка на объект. ZGC перемещает его, и происходит переключение контекста, когда поток приложения запускается и пытается получить доступ к этому объекту через его старый адрес. Для решения этой проблемы ZGC использует барьеры нагрузки. Барьер загрузки – это фрагмент кода, который выполняется, когда поток загружает ссылку из кучи – например, когда мы обращаемся к непримитивному полю объекта.
В ZGC барьеры загрузки проверяют биты метаданных ссылки. В зависимости от этих битов ZGC может выполнить некоторую обработку ссылки, прежде чем мы ее получим. Таким образом, это может привести к совершенно другой ссылке. Мы называем это переназначением.
3.2. Маркировка
ZGC разбивает маркировку на три этапа.
Первая фаза-это фаза остановки мира. На этом этапе мы ищем корневые ссылки и помечаем их. Корневые ссылки-это отправные точки для доступа к объектам в куче , например, к локальным переменным или статическим полям. Поскольку количество корневых ссылок обычно невелико, эта фаза коротка.
Следующий этап-параллельный. На этом этапе мы проходим по графу объектов, начиная с корневых ссылок. Мы отмечаем каждый объект, до которого добираемся. Кроме того, когда барьер нагрузки обнаруживает немаркированную ссылку, он также помечает ее.
Последняя фаза также является фазой остановки мира для обработки некоторых крайних случаев, таких как слабые ссылки.
В этот момент мы знаем, до каких объектов мы можем дотянуться.
ZGC использует помеченные 0 и помеченные 1 биты метаданных для маркировки.
3.3. Эталонная окраска
Ссылка представляет положение байта в виртуальной памяти. Однако для этого нам не обязательно использовать все биты ссылки – некоторые биты могут представлять свойства ссылки . Это то, что мы называем эталонной раскраской.
С 32 битами мы можем адресовать 4 гигабайта. Поскольку в настоящее время широко распространено, чтобы компьютер имел больше памяти, чем это, мы, очевидно, не можем использовать ни один из этих 32 бит для раскраски. Поэтому ZGC использует 64-разрядные ссылки. Это означает, что ZGC доступен только на 64-разрядных платформах:
Ссылки ZGC используют 42 бита для представления самого адреса. В результате ссылки ZGC могут обращаться к 4 терабайтам пространства памяти.
Кроме того, у нас есть 4 бита для хранения ссылочных состояний:
- финализуемый бит – объект доступен только через финализатор
- remap bit – ссылка актуальна и указывает на текущее местоположение объекта (см. перемещение)
- marked0 и marked1 биты – они используются для маркировки достижимых объектов
Мы также назвали эти биты битами метаданных. В ZGC точно один из этих битов метаданных равен 1.
3.4. Переезд
В ZGC переезд состоит из следующих этапов:
- Параллельная фаза, которая ищет блоки, которые мы хотим переместить, и помещает их в набор перемещений.
- Фаза остановки мира перемещает все корневые ссылки в наборе перемещений и обновляет их ссылки.
- Параллельная фаза перемещает все оставшиеся объекты в наборе перемещений и сохраняет сопоставление между старым и новым адресами в таблице пересылки.
- Переписывание оставшихся ссылок происходит на следующем этапе маркировки. Таким образом, нам не придется дважды пересекать дерево объектов. В качестве альтернативы это могут сделать и барьеры нагрузки.
3.5. Переназначение и барьеры нагрузки
Обратите внимание, что на этапе перемещения мы не переписывали большинство ссылок на перемещенные адреса. Поэтому, используя эти ссылки, мы не будем получать доступ к объектам, которые нам нужны. Хуже того, мы могли бы получить доступ к мусору.
ZGC использует барьеры нагрузки для решения этой проблемы. Барьеры загрузки исправляют ссылки, указывающие на перемещенные объекты, с помощью метода, называемого переназначением.
Когда приложение загружает ссылку, оно запускает барьер загрузки, который затем выполняет следующие действия, чтобы вернуть правильную ссылку:
- Проверяет, установлен ли бит remap в 1. Если это так, это означает, что ссылка актуальна, поэтому мы можем безопасно вернуть ее.
- Затем мы проверяем, был ли указанный объект в наборе перемещений или нет. Если это не так, значит, мы не хотели его переносить. Чтобы избежать этой проверки при следующей загрузке этой ссылки, мы устанавливаем бит remap равным 1 и возвращаем обновленную ссылку.
- Теперь мы знаем, что объект, к которому мы хотим получить доступ, был целью перемещения. Вопрос только в том, произошло ли переселение или нет? Если объект был перемещен, мы переходим к следующему шагу. В противном случае мы переместим его сейчас и создадим запись в таблице пересылки, в которой будет храниться новый адрес для каждого перемещенного объекта. После этого мы переходим к следующему шагу.
- Теперь мы знаем, что объект был перемещен. Либо ZGC, нами на предыдущем шаге, либо барьером нагрузки во время более раннего попадания этого объекта. Мы обновляем эту ссылку на новое местоположение объекта (либо с помощью адреса из предыдущего шага, либо путем поиска его в таблице пересылки), устанавливаем бит remap и возвращаем ссылку.
И все, с помощью описанных выше шагов мы гарантировали, что каждый раз, когда мы пытаемся получить доступ к объекту, мы получаем самую последнюю ссылку на него. Поскольку каждый раз, когда мы загружаем ссылку, она запускает барьер загрузки. Поэтому это снижает производительность приложения. Особенно в первый раз, когда мы получаем доступ к перемещенному объекту. Но это цена, которую мы должны заплатить, если хотим сделать короткую паузу. И поскольку эти шаги относительно быстры, это не оказывает существенного влияния на производительность приложения.
4. Как включить ZGC?
Мы можем включить ZGC с помощью следующих параметров командной строки при запуске нашего приложения:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
Обратите внимание, что, поскольку ZGC является экспериментальным GC, потребуется некоторое время, чтобы получить официальную поддержку.
5. Заключение
В этой статье мы увидели, что ZGC намерена поддерживать большие размеры кучи с низким временем паузы приложения.
Для достижения этой цели он использует методы, включая цветные 64-разрядные ссылки, барьеры нагрузки, перемещение и переназначение.