Как настроить память для JVM
Некоторые утверждают, что настройки можно определить автоматически, но звучит это сомнительно, потому что JVM — довольно сложная штука.
Память JVM разбита на несколько областей:
- Куча (heap). В ней хранятся основные данные и работает сборщик мусора (GC), накладные расходы на который бывают до 10%. Кучу можно зарезервировать сразу всю флагом (
-XX:+AlwaysPreTouch
), а можно динамически уменьшать и отдавать системе (-XX:UseAdaptiveSizePolicy
), поигравшись с настройками зарезервированного запаса, но это работает не с любым GC. Источник проблем в этой области — большие объемы данных. - Метаданные классов. Всякие структуры классов, символы, константы, аннотации и т.п. В поздних версиях разделена на непрерывную область для классов (чтобы можно было сжимать указатели) и область для остальных метаданных. Гадостей в этой области может наделать загрузчик классов, генерирующий кучу классов на каждый чих.
- Область JIT-компилятора. Включает в себя скомпилированные куски кода и “арены” компилятора (где хранятся необходимые данные для компиляции: промежуточные представления, статистика и т.п.). Если включена многоуровневая компиляция (по умолчанию это так), то размер области под скомпилированный код увеличивается. Опасность плохих настроек тут в том, что область забьется, и код будет компилироваться и вытесняться по кругу, сжирая процессор. В стабильной системе JIT прогревается на старте и потом почти не вызывается.
- Потоки со стеками вызовов. По утверждению докладчика, тут обычно волноваться не о чем.
- Таблица символов и таблица строк. Тут тоже редко бывают проблемы.
- Внутренняя память. Содержит память, выделенную вне области с GC: ByteBuffer’ы (есть настройка для ограничения) и замапленные в память файлы (нельзя ограничить по памяти). Причем у буферов могут быть кэши в куче, которые еще локальны для потока и никогда не чистятся (но можно ограничить их размер). При аллокации буферов могут возникнуть проблемы с фрагментацией, поэтому стандартный аллокатор имеет смысл заменить, например на jemalloc.
Найти проблемы с утечками памяти можно с помощью async-profiler, который может перехватывать вызовы malloc
и определять стектрейс к ним.
В итоге, чтобы определить проблемные места, нужно мониторить все метрики (в том числе включить логи GC и смотреть на метрики операционной системы) и подключать Native Memory Tracking для исследования тяжелых случаев. Посчитать итоговую формулу для контейнера нельзя. Хотя по опыту коллег примерно половина памяти контейнера уходит на память вне кучи.