Некоторые утверждают, что настройки можно определить автоматически, но звучит это сомнительно, потому что JVM — довольно сложная штука.

Память JVM разбита на несколько областей:

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

Найти проблемы с утечками памяти можно с помощью async-profiler, который может перехватывать вызовы malloc и определять стектрейс к ним.

В итоге, чтобы определить проблемные места, нужно мониторить все метрики (в том числе включить логи GC и смотреть на метрики операционной системы) и подключать Native Memory Tracking для исследования тяжелых случаев. Посчитать итоговую формулу для контейнера нельзя. Хотя по опыту коллег примерно половина памяти контейнера уходит на память вне кучи.