Начнём с простого вопроса. В самом ли деле каждый Java-разработчик понимает, как в Java работает память? Одна из обязанностей любого Java-разработчика — гарантировать, что в результате тонкой настройки приложения на Java из него получится выжать такую производительность, какую только возможно. Требуется время, чтобы научиться управлять памятью в Java и понять этот процесс, это касается всех, кто имеет дело с Java. В этой статье попробую объяснить, как овладеть этими умениями.

Суть в том, чтобы правильно организовать в языке Java выделение новых объектов и удаление неиспользуемых; последний процесс чаще называется «сборкой мусора» (Garbage Collection). По умолчанию Java и автоматически довольно хорошо справляется с управлением памятью. Дело в том, что сборщик мусора постоянно работает в фоновом режиме и подчищает неиспользуемые объекты, а также те, на которые не стоят ссылки, чтобы высвободить некоторую долю памяти.

Разработчику просто не обойтись без достаточного опыта и знаний о том, как в Java используется память, как именно работают виртуальная машина Java (JVM) и сборщик мусора. Не зная этого, не найти и не устранить узких мест, в поиске которых мы выполняем код и тестируем его производительность.

Весь мой опыт теории и практики программирования, изучение устройства этого языка, стремление разобраться в технических проблемах и многолетний опыт хостинга многочисленных серверов Minecraft постепенно привели меня к пониманию, как виртуальная машина Java работает в различных окружениях. Когда это приключение только начиналось, я даже не был уверен, как именно и где именно создаётся объект Java. Я также не вполне понимал, как различные сборщики мусора подчищают различные объекты, на которые не стоят ссылки, как это делается в различных областях динамической памяти (кучи) Java.

Когда я впервые попытался оптимизировать несколько серверов Minecraft (повысить их производительность), я столкнулся с несколькими ошибками, возникающими при работе с памятью, в частности, с java.lang.OutOfMemoryError. Именно тогда я стал гораздо лучше понимать, какие роли отводятся стеку и куче при оптимизации памяти в Java.

Вот важная вещь, которую нужно учитывать, рассуждая о работе с памятью в Java: сначала нужно всегда запускать виртуальную машину Java, а затем настраивать её, добиваясь максимально возможной производительности. Создание любого класса, метода, объекта или переменной – это работа с памятью. Таким образом, вся информация сохраняется в динамической памяти (куче) Java.

Динамическая память Java


Думаю, большинству из вас доводилось видеть десять-двадцать вариантов схем, объясняющих, как устроена динамическая память в Java. Любому Java-разработчику необходимо понимать, какова разница между PermGen и Metaspace в новейших релизах инструментария для разработки на Java (JDK). К этим схемам мы обратимся чуть ниже.

image

image
image
Динамическая память делится на два поколения. Первое называется young, а второе old. Первая крупная часть называется пространством eden, а вторая — пространством survivor. Пространство survivor состоит из подпространств survivor0 и survivor1.

Теперь давайте объясним, зачем требуется каждое из пространств в поколении young. Все объекты, которые мы создаём, сначала сохраняются в пространстве eden. В виртуальной машине Java по умолчанию включается автоматическое управление памятью.

В случаях, когда в приложении очень много объектов — скажем, они создаются тысячами — память eden будет целиком заполнена объектами. Как только сборщик мусора это заметит, он удалит все неиспользуемые объекты, либо те объекты, на которые отсутствуют ссылки. Такой механизм называется «малый сборщик мусора» (Minor Garbage Collector). Малый сборщик мусора перенесёт все уцелевшие объекты в пространство survivor. Из этого сделаем такой вывод: поколение young автоматически подпадает под сборку мусора, чтобы по мере необходимости высвобождать память. Эта операция будет выполняться быстро и за короткий срок.

Учитывая, что в разных классах в коде приложения может создаваться такое множество объектов, будет увеличиваться пространство, выделяемое в памяти под eden. Здесь можно предположить, что сработают GC1, GC2 и GC3, а возможно — и многие другие. Когда в виртуальной машине Java в таком огромном количестве вызываются операции сборки мусора, малому сборщику мусора ничего не остаётся, кроме как проверять все объекты, готовые к перемещению в survivor. Наконец, виртуальная машина Java будет готова переместить все оставшиеся объекты в пространство памяти survivor.

Уцелевшие объекты из поколения young перемещаются в поколение old, а когда пространство old заполняется объектами, включается главный сборщик мусора.

Главный сборщик мусора


Итак, работа главного сборщика мусора начинается, когда поколение old в памяти целиком заполняется объектами. Обратите внимание: на запуск главного сборщика мусора требуется некоторое время. Когда Java-разработчик (или команда разработчиков) пишет приложение с нуля или автоматизирует эту работу при помощи некоторого фреймворка, приходится исключительно тщательно обращаться с поколениями young и old.

Серьёзная вещь, которую требуется продумать при разработке на Java — постараться не создавать ненужных объектов, которые всё равно использовались бы в коде вашего приложения нечасто. Ведь если вы создадите какие-либо объекты, сборщик мусора их утилизирует, как только они выполнят назначенные им задачи. Чтобы было проще понимать работу сборщика мусора, можно сформулировать его назначение так:

Малый сборщик мусора нацелен на переработку поколения young, а поколение old подпадает под действия главного сборщика мусора.

Если рассмотреть в качестве примера, как устроены сайты Amazon или Walmart, окажется, что с них на веб-сервер поступает огромное количество запросов. При активном трафике на фоне обработки запросов начнут просматриваться задержки. Дело в том, что главный сборщик мусора занимает в памяти достаточно много места — это необходимо, чтобы уничтожить неиспользуемые объекты. Побочный эффект в данном случае — возникает большая нагрузка на ЦП и ОЗУ на тех узлах, которые обслуживают сайт(ы).

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

Ошибки, связанные с памятью


Рекомендую Java-разработчику научиться читать и понимать вывод сообщений об ошибках — ведь по ним можно хорошо понять, что происходит с виртуальной машиной Java, это не ошибки в самом коде приложения, который на ней выполняется. Фактическая причина той ошибки в коде, которую вы наблюдаете — это, например, утечка памяти, сбой в работе сборщика мусора, она может быть связана с выделением ресурсов и даже с проблемами, возникающими при синхронизации. Сначала нужно научиться решать такие проблемы, ведь так мы сможем быстро ограничить зону поражения ресурсов.

Обратите внимание: нам также потребуется отслеживать, как используются ресурсы. Крайне рекомендуется тестировать производительность приложений Java. Для этого можно, например, профилировать каждую категорию, многократно снимать дамп кучи, проверять и отлаживать код приложений и делать многое другое. Если не одна из этих мер не сработает, то обычно рекомендуется просто выделять вашему приложению больше ресурсов. Ниже перечислены распространённые ошибки и рассказано, что при них происходит.
  • java.lang.StackOverFlowError — память стека заполнена.
  • java.lang.OutOfMemoryError — память кучи заполнена.
  • java.lang.OutOfMemoryError: GC Overhead limit exceeded — сборщик мусора израсходовал лимит допустимых издержек
  • java.lang.OutOfMemoryError: Permgen space — заполнено пространство «постоянного поколения» (Permanent Generation)
  • java.lang.OutOfMemoryError: Metaspace — заполнено метапространство (Metaspace, используется в Java JDK 8 и выше)
  • java.lang.OutOfMemoryError: Unable to create new native thread — это значит, что нативный код JVM больше не в состоянии создавать новые нативные потоки в базовой операционной системе, поскольку уже создано слишком много потоков, и они потребляют всю доступную память, выделенную для виртуальной машины Java.
  • java.lang.OutOfMemoryError: request size bytes for reason — такая ошибка означает, что приложение полностью израсходовало пространство, выделенное для подкачки
  • java.lang.OutOfMemoryError: Requested array size exceeds VM limit — это значит, что массив, используемый вашим приложением, превышает максимальный размер, допустимый на данной платформе.

Установка исходного и максимального размера кучи


Исходный размер кучи — XMS

Это исходный размер кучи. Под неё обычно выделяется 1/64-я всей оперативной памяти, имеющейся на используемом вами узле. Обычно я в таком случае устанавливаю значение 128MB. Это значение можно переопределить через командную строку при помощи опции java -Xms128M.

Максимальный размер кучи — XMX

Это максимум памяти, который можно выделить под кучу. Он тем меньше, чем меньше общая мощность ОЗУ на используемом вами узле. Как правило, здесь я устанавливаю по умолчанию значение 8192MB. Это значение также можно переопределить через командную строку при помощи опции java -Xmx8192M.

Вот полный пример команды, запускающей приложение Java.

java -Xms128M -Xmx8192M app.jar

Исходное и максимальное значение можно менять, исходя из нужд приложения. Я обычно подбираю значения по умолчанию, которые присваиваю всем тем Java-приложениям, с которыми работаю. Мне, как правило, приходится иметь дело с серверами Minecraft, а пользователи Minecraft обычно нуждаются не более чем в 8 ГБ памяти на своем Minecraft-сервере. Я выделяю им дополнительно ещё по 1GB памяти на выполнение фоновых задач, например, на сборку мусора.

Дополнительные опции

  • -XX:+UseG1GC: активирует первичный сборщик мусора (G1), который предназначен для работы с такими приложениями, где куча большая, а при сборке мусора допускается сравнительно большая задержка.
  • -XX:+UseZGC: активирует Z-сборщик мусора, предназначенный для работы с такими приложениями, в которых требуется снизить задержку, не жертвуя при этом пропускной способностью.
  • -XX:+UseShenandoahGC: активирует the Shenandoah Garbage Collector («самодельный сборщик мусора»), цель которого – сократить паузы на сборку мусора, по возможности распараллеливая эту работу в разных потоках приложения.
  • -XX:+UseParallelGC: активирует параллельный сборщик мусора для поколения young.
  • -XX:NewRatio=: устанавливает соотношение между размерами «старого» и «молодого» поколения. Например, при значении -XX:NewRatio=3 старое поколение будет втрое больше молодого.
  • -XX:SurvivorRatio=: устанавливает соотношение пространств eden/survivor. Снижая это соотношение, мы увеличиваем размер пространства, выделенного под уцелевших.
  • -XX:MaxGCPauseMillis: устанавливает целевое значение для максимальной паузы в работе сборщика мусора. Это нежёсткая цель, и виртуальная машина Java будет стремиться её достичь, насколько это возможно.
  • -XX:+UseSerialGCx: активирует последовательный сборщик мусора, выполняющий всю работу в единственном потоке и обычно подходящий для небольших приложений, занимающих в памяти не так много места.
  • -XX:ParallelGCThreads: задаёт, сколько потоков будет задействоваться при параллельной работе сборщиков мусора. Значение, задаваемое по умолчанию, варьируется в зависимости от той платформы, на которой работает виртуальная машина Java.
  • -XX:ConcGCThreads: количество потоков, которые будут использовать конкурентные сборщики мусора. Значение, задаваемое по умолчанию, варьируется в зависимости от той платформы, на которой работает виртуальная машина Java.
  • -XX:InitiatingHeapOccupancyPercentx: процент, на который должна быть заполнена куча, чтобы запустились конкурентные циклы сборки мусора.
  • -XX:+HeapDumpOnOutOfMemoryError: приказывает виртуальной машине Java сгенерировать дамп кучи в случае, когда выброшено исключение OutOfMemoryError.
  • -XX:+PrintGCDetails: выводит подробный отчёт по каждой операции сборки мусора. Используется для тонкой настройки сборщика мусора.
  • -XX:+PrintGCDateStamps: добавляет метку времени к каждому акту сборки мусора, и затем эти метки выводятся в логах вместе с самими событиями.
  • -XX:+PrintHeapAtGC: выводит подробную информацию о куче до и после сборки мусора.
  • -XX:+PrintGCApplicationStoppedTime: выводит, сколько времени было потрачено на паузы в работе сборщика мусора, помогая таким образом выяснить, когда именно возникают такие паузы.
  • -XX:+PrintGCApplicationConcurrentTime: сообщает, какая часть потраченного времени ушла не на сборку мусора (то есть, сколько времени работало приложение).
  • -XX:+UseCodeCacheFlushing: позволяет виртуальной машине Java очищать кэш с кодом, когда тот заполнится. Так предотвращается аварийная остановка виртуальной машины Java, возможная из-за такого переполнения.

Заключение


Легко можно насчитать 500+ аргументов, которые можно было бы передать виртуальной машине Java для тонкой настройки сборки мусора и памяти Java для работы с приложениями. Если вы хотите управлять и другими аспектами, то количество таких аргументов легко превысит 1000. Здесь я объяснил лишь очень немногие аргументы, которые помогают решать наиболее острые вопросы, связанные с управлением приложениями Java и прочими программами.

Комментарии (4)


  1. dyadyaSerezha
    26.04.2024 13:52
    +1

    подчищает неиспользуемые объекты, а также те, на которые не стоят ссылки

    Вот даже как. И в чем же различие между ними?


  1. AlexunKo
    26.04.2024 13:52
    +7

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

    Ложное убеждение. Не советую.


    1. webaib1
      26.04.2024 13:52
      +2

      Как раз зашёл чтобы такой же комментарий оставить.

      В сотнях проектах, которые я видел или участвовал, главное не говнокодить. Это значит уметь понять структуру приложения, если таковая есть и не ломать ее. Если ее нет, то создать ее или просто выжить в проекте. Потом эффективность и скорость по памяти и времени и т.д.

      Я в да же в хл не вижу смысла лезть в настройки памяти, кроме может соотношения система/вм.


  1. velon
    26.04.2024 13:52
    +7

    Я по этой теме посоветовал бы цикл статей на Хабре "Дюк, вынеси мусор", вот начало: Введение