Некоторые из вас были там. Вы добавили опцию -Xmx в свои сценарии запуска и расслабились, зная, что ваш Java-процесс никак не будет поглощать больше памяти, чем разрешил ваш тонко настроенный параметр. А потом ты был готов к неприятному сюрпризу. Либо самостоятельно, проверив таблицу процессов в своем блоке разработки / тестирования, либо, если что-то пошло не так, с помощью операций, которые звонят вам посреди ночи и говорят, что память 4G, которую вы запросили для производства, исчерпана. И это приложение только что умерло.
Так что, черт возьми, происходит под капотом? Почему процесс потребляет больше памяти, чем выделено? Это ошибка или что-то совершенно нормальное? Терпите меня, и я проведу вас через то, что происходит.
Во-первых, частью этого может быть злонамеренный нативный код с утечкой памяти. Но в 99% случаев это совершенно нормальное поведение JVM. То, что вы указали с помощью ключей -Xmx, ограничивает объем памяти, используемой вашей кучей приложений .
Помимо кучи, в памяти есть другие области, которые ваше приложение использует внутри, а именно: permgen и размеры стека. Поэтому, чтобы ограничить их, вы также должны указать опции -XX: MaxPermSize и -Xss соответственно. Короче говоря, вы можете предсказать использование памяти вашего приложения с помощью следующей формулы
1
|
Max memory = [-Xmx] + [-XX:MaxPermSize] + number_of_threads * [-Xss] |
Но помимо памяти, используемой вашим приложением, сама JVM также нуждается в некотором свободном пространстве . Необходимость в этом вытекает из нескольких разных причин:
- Вывоз мусора. Как вы помните, Java — это язык для сборки мусора. Чтобы сборщик мусора знал, какие объекты пригодны для сбора, он должен отслеживать графы объектов. Так что это одна часть памяти, потерянной для этой внутренней бухгалтерии. Особенно G1 известен своим чрезмерным аппетитом к дополнительной памяти, так что имейте это в виду.
- JIT оптимизация. Виртуальная машина Java оптимизирует код во время выполнения. Опять же, чтобы знать, какие части оптимизировать, необходимо отслеживать выполнение определенных частей кода. Итак, снова вы потеряете память.
- Распределение в куче. Если вам случается использовать память вне кучи, например, при использовании прямого или отображенного ByteBuffers самостоятельно или через какой-то умный сторонний API, то вуаля — вы расширяете свою кучу до чего-то, что вы фактически не можете контролировать с помощью конфигурации JVM.
- Код JNI. Когда вы используете собственный код, например, в формате драйверов баз данных типа 2 , вы снова загружаете код в собственную память.
- Метапространство. Если вы ранний пользователь Java 8, вы используете metaspace вместо старого доброго permgen для хранения объявлений классов. Это не ограничено и в родной части JVM.
Вы также можете использовать память по другим причинам, кроме перечисленных выше, но я надеюсь, что мне удалось убедить вас в том, что внутренняя часть JVM съела значительное количество памяти. Но есть ли способ предсказать, сколько памяти на самом деле понадобится? Или хотя бы понять, куда он исчезает для оптимизации?
Как мы выяснили из болезненного опыта — предсказать это с достаточной точностью невозможно. Накладные расходы JVM могут варьироваться от нескольких процентов до нескольких сотен процентов. Ваш лучший друг снова старый добрый метод проб и ошибок. Поэтому вам необходимо запустить приложение с нагрузками, аналогичными производственной среде и измерениям.
Измерение дополнительных издержек тривиально — просто контролируйте процесс с помощью встроенных инструментов ОС (в верхней части Linux, Activity Monitor в OS X, Task Manager в Windows), чтобы узнать реальное потребление памяти. Вычтите размеры кучи и permgen из реального потребления, и вы увидите накладные расходы.
Теперь, если вам нужно сократить накладные расходы, вы бы хотели понять, где они на самом деле исчезают. Мы обнаружили, что vmmap в Mac OS X и pmap в Linux являются действительно полезными инструментами в этом случае. Мы сами не использовали порт vmmap для Windows, но, похоже, есть инструмент для фанатов Windows.
Следующий пример иллюстрирует эту ситуацию. Я запустил свой Jetty со следующими параметрами запуска:
1
|
-Xmx168m -Xms168m -XX:PermSize=32m -XX:MaxPermSize=32m -Xss1m |
Зная, что в моем приложении запущено 30 потоков, я могу ожидать, что использование моей памяти не превысит 230M, несмотря ни на что. Но теперь, когда я смотрю на Activity Monitor на моем Mac OS X, я вижу что-то другое
Реальное использование памяти превысило 320 миллионов. Теперь, копаясь под капотом, как происходит процесс с помощью вывода vmmap <pid>, мы начинаем понимать, куда исчезает память. Давайте рассмотрим несколько примеров:
Следующее говорит, что мы потеряли около 2 МБ и потерялись в памяти отображаемой библиотеки rt.jar
1
|
mapped file 00000001178b9000-0000000117a88000 [ 1852K] r-- /r-x SM=ALI /Library/Java/JavaVirtualMachines/jdk1 .7.0_21.jdk /Contents/Home/jre/lib/rt .jar |
В следующем разделе объясняется, что мы используем ~ 6 МБ для конкретной загруженной динамической библиотеки.
1
|
__TEXT 0000000104573000-0000000104c00000 [ 6708K] r-x /rwx SM=COW /Library/Java/JavaVirtualMachines/jdk1 .7.0_21.jdk /Contents/Home/jre/lib/server/libjvm .dylib |
И здесь у нас есть потоки № 25-30, каждый из которых выделяет 1 МБ для своих стеков и стековых охранников.
1
|
Stack 000000011a5f1000-000000011a6f0000 [ 1020K] rw- /rwx SM=ZER thread 25 Stack 000000011aa8c000-000000011ab8b000 [ 1020K] rw- /rwx SM=ZER thread 27 Stack 000000011ab8f000-000000011ac8e000 [ 1020K] rw- /rwx SM=ZER thread 28 Stack 000000011ac92000-000000011ad91000 [ 1020K] rw- /rwx SM=ZER thread 29 Stack 000000011af0f000-000000011b00e000 [ 1020K] rw- /rwx SM=ZER thread 30 |
1
|
STACK GUARD 000000011a5ed000-000000011a5ee000 [ 4K] --- /rwx SM=NUL stack guard for thread 25 STACK GUARD 000000011aa88000-000000011aa89000 [ 4K] --- /rwx SM=NUL stack guard for thread 27 STACK GUARD 000000011ab8b000-000000011ab8c000 [ 4K] --- /rwx SM=NUL stack guard for thread 28 STACK GUARD 000000011ac8e000-000000011ac8f000 [ 4K] --- /rwx SM=NUL stack guard for thread 29 STACK GUARD 000000011af0b000-000000011af0c000 [ 4K] --- /rwx SM=NUL stack guard for thread 30 |
Надеюсь, мне удалось пролить свет на сложную задачу прогнозирования и измерения фактического потребления памяти.