Почему Sun JVM продолжает потреблять все больше памяти RSS, даже когда размеры кучи и т.д. Стабильны?
За последний год я сделал огромные улучшения в моем применении Java-кучи - твердое 66% -ное сокращение. Для этого я отслеживал различные показатели, такие как размер кучи Java, процессор, Java-кучу и т.д. Через SNMP.
Недавно я отслеживал, сколько реальной памяти (RSS, резидентный набор) JVM и я несколько удивлен. Реальная память, потребляемая JVM, кажется совершенно независимой от моего размера кучи приложений, не-кучи, пространства eden, количества потоков и т.д.
Размер кучи, измеренный Java SNMP
График использования Java Heap http://lanai.dietpizza.ch/images/jvm-heap-used.png
Реальная память в КБ. (Например: 1 МБ КБ = 1 ГБ)
Используемая графа Java Heap http://lanai.dietpizza.ch/images/jvm-rss.png
(Три провала в графе кучи соответствуют обновлениям/перезапускам приложений.)
Это проблема для меня, потому что вся эта дополнительная память, которую JVM потребляет, - это "кража" памяти, которая может использоваться ОС для кэширования файлов. Фактически, как только значение RSS достигает ~ 2,5-3 ГБ, я начинаю видеть более медленное время отклика и более высокую загрузку процессора из моего приложения, в основном, для ожидания ввода-вывода. Когда какой-то пункт разбивается на страницы подкачки. Это очень нежелательно.
Итак, мои вопросы:
- Почему это происходит? Что происходит "под капотом"?
- Что я могу сделать, чтобы сохранить реальное потребление памяти JVM?
Детали gory:
- RHEL4 64-bit (Linux - 2.6.9-78.0.5.ELsmp # 1 SMP Wed Sep 24... 2008 x86_64... GNU/Linux)
- Java 6 (сборка 1.6.0_07-b06)
- Tomcat 6
- Приложение (потоковое видео HTTP по запросу)
- Высокий ввод-вывод через java.nio FileChannels
- Сотни до низких тысяч потоков
- Низкое использование базы данных
- Spring, Hibernate
Соответствующие параметры JVM:
-Xms128m
-Xmx640m
-XX:+UseConcMarkSweepGC
-XX:+AlwaysActAsServerClassMachine
-XX:+CMSIncrementalMode
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCApplicationStoppedTime
-XX:+CMSLoopWarn
-XX:+HeapDumpOnOutOfMemoryError
Как я измеряю RSS:
ps x -o command,rss | grep java | grep latest | cut -b 17-
Это входит в текстовый файл и регулярно считывается в базу данных RRD через систему мониторинга. Обратите внимание, что ps выводит Kilo Bytes.
Проблема и решения:
В конце концов, это был ATorras ответ, который оказался в конечном счете правильным, kdgregory, который направил меня к правильному пути диагностики с использованием pmap
. (Проголосуйте за оба ответа!) Вот что происходит:
То, что я точно знаю:
- Мои приложения записывают и отображают данные с JRobin 1.4, что я закодировал в своем приложении более трех лет назад.
- Самый загруженный экземпляр приложения в настоящее время создает
- Более 1000 новых файлов базы данных JRobin (около 1,3 МБ каждый) в течение часа после запуска
- ~ 100 + каждый день после запуска
- Приложение обновляет эти объекты базы данных JRobin один раз каждые 15 секунд, если есть что писать.
- В конфигурации по умолчанию JRobin:
- использует back-end для доступа к файлу
java.nio
. Этот back-end отображает MappedByteBuffers
на сами файлы.
- раз в пять минут поток демона JRobin вызывает
MappedByteBuffer.force()
на каждой базовой базе данных JRobin MBB
-
pmap
:
- 6500 сопоставлений
- 5500 из которых - файлы базы данных JRobin объемом 1,3 МБ, которые работают до ~ 7.1 ГБ.
Этот последний момент был моей "Эврика!" момент.
Мои корректирующие действия:
- Рассмотрим обновление до последней версии JRobinLite 1.5.2, которая, по-видимому, лучше
- Реализовать правильную обработку ресурсов в базах данных JRobin. На данный момент, когда мое приложение создает базу данных, а затем никогда не выгружает ее после того, как база данных больше не используется активно.
- Экспериментируйте с перемещением событий
MappedByteBuffer.force()
к обновлению базы данных, а не периодическим таймером. Будет ли проблема волшебно уходить?
- Непосредственно измените исходный код JRobin на реализацию java.io - изменение строки. Это будет медленнее, но это, возможно, не проблема. Вот график, показывающий непосредственное влияние этого изменения.
Используемая память в формате Java RSS http://lanai.dietpizza.ch/images/stackoverflow-rss-problem-fixed.png
Вопросы, которые я могу или не могу успеть выяснить:
- Что происходит внутри JVM с помощью
MappedByteBuffer.force()
? Если ничего не изменилось, все равно записывает весь файл? Часть файла? Загружает ли он его первым? - Всегда есть ли в MBB определенное количество MBB? (RSS был примерно наполовину общим распределенным размером MBB. Совпадение? Я подозреваю, что нет.)
- Если я переведу
MappedByteBuffer.force()
на события обновления базы данных, а не на периодический таймер, проблема будет волнующе исчезнуть?
- Почему RSS-склон был настолько регулярным? Он не коррелирует с какой-либо из показателей нагрузки приложения.
Ответы
Ответ 1
Просто идея: буферы NIO размещаются за пределами JVM.
EDIT:
По состоянию на 2016 год стоит рассмотреть комментарий @Lari Hotari [Почему Sun JVM продолжает потреблять все больше RSS-памяти, даже когда размеры кучи и т.д. Стабильны?], потому что назад к 2009, RHEL4 имел glibc < 2.10 (~ 2.3)
С уважением.
Ответ 2
RSS представляет собой страницы, которые активно используются - для Java, это прежде всего живые объекты в куче и внутренние структуры данных в JVM. Там мало что можно сделать, чтобы уменьшить его размер, за исключением использования меньшего количества объектов или меньшего объема обработки.
В вашем случае я не думаю, что это проблема. Похоже, что график отображает 3 мегабайта, а не 3 гига, которые вы пишете в тексте. Это действительно мало, и вряд ли это вызовет пейджинг.
Так что же еще происходит в вашей системе? Это ситуация, когда у вас много серверов Tomcat, каждый из которых потребляет 3 миллиона RSS? Вы бросаете много флагов GC, они показывают, что процесс тратит большую часть времени в GC? У вас есть база данных, работающая на одном компьютере?
Изменить в ответ на комментарии
Что касается размера 3M RSS - да, это казалось слишком низким для процесса Tomcat (я проверил свою коробку и имел один на 89M, который неактивен некоторое время). Тем не менее, я не обязательно ожидаю, что это будет > размер кучи, и я, конечно же, не ожидаю, что он будет почти в 5 раз больше размера кучи (вы используете -Xmx640) - он должен в худшем случае быть размером кучи + несколько за приложение константа.
Это заставляет меня подозревать ваши цифры. Таким образом, вместо графика с течением времени запустите снимок (замените 7429 любым идентификатором процесса, который вы используете):
ps -p 7429 -o pcpu,cutime,cstime,cmin_flt,cmaj_flt,rss,size,vsize
(Edit by Stu, поэтому мы можем сформировать результаты для вышеуказанного запроса для ps info:)
[[email protected] ~]$ ps -p 12720 -o pcpu,cutime,cstime,cmin_flt,cmaj_flt,rss,size,vsize
%CPU - - - - RSS SZ VSZ
28.8 - - - - 3262316 1333832 8725584
Изменить, чтобы объяснить эти числа для потомков
RSS, как уже отмечалось, представляет собой резидентный размер набора: страницы в физической памяти. SZ содержит количество страниц, доступных для записи процессом (плата за совершение транзакции); manpage описывает это значение как "очень грубое". VSZ имеет размер карты виртуальной памяти для процесса: записываемые страницы плюс общие страницы.
Обычно VSZ немного > SZ, и очень много > RSS. Этот вывод указывает на очень необычную ситуацию.
Разработка того, почему единственным решением является сокращение объектов
RSS представляет количество страниц, находящихся в ОЗУ, - страницы, к которым активно обращаются. С помощью Java сборщик мусора будет периодически перемещаться по всему графику объекта. Если этот объектный граф занимает большую часть пространства кучи, коллекционер коснется каждой страницы в куче, требуя, чтобы все эти страницы стали резидентными. GC очень хорош в уплотнении кучи после каждой основной коллекции, поэтому, если вы работаете с частичной кучей, большинство страниц не должно быть в ОЗУ.
И некоторые другие опции
Я заметил, что вы упомянули о том, что у вас сотни тысяч нитей. Стеки для этих потоков также добавят в RSS, хотя это не должно быть много. Предполагая, что потоки имеют глубину неглубокого вызова (типичные для потоков обработчика приложений-приложений), каждый должен потреблять только одну или две страницы физической памяти, даже если для каждого из них взимается плата за полминуты.
Ответ 3
Почему это происходит? Что происходит "под капотом"?
JVM использует больше памяти, чем просто кучу. Например, методы Java, стеки потоков и собственные дескрипторы выделяются в памяти отдельно от кучи, а также внутренние структуры данных JVM.
В вашем случае возможными причинами проблем могут быть: NIO (уже упоминалось), JNI (уже упоминалось), чрезмерное создание потоков.
О JNI, вы написали, что приложение не использует JNI, но... Какой тип драйвера JDBC вы используете? Может быть, это тип 2 и утечка? Это очень маловероятно, хотя, как вы сказали, использование базы данных было низким.
О чрезмерном создании потоков, каждый поток получает свой собственный стек, который может быть довольно большим. Размер стека фактически зависит от VM, ОС и архитектуры, например. для JRockit it 256K на Linux x64, я не нашел ссылку в документации Sun для Sun VM. Это напрямую влияет на память потоков (количество потоков потока = размер стека потока * количество потоков). И если вы создаете и уничтожаете много потоков, память, вероятно, не будет повторно использована.
Что я могу сделать, чтобы сохранить реальное потребление памяти JVM?
Честно говоря, сотни-тысячи тысяч нитей кажутся мне огромными. Тем не менее, если вам действительно нужно столько потоков, размер стека потоков можно настроить с помощью опции -Xss
. Это может снизить потребление памяти. Но я не думаю, что это решит всю проблему. Я склонен думать, что где-то есть утечка, когда я смотрю на реальный график памяти.
Ответ 4
Текущий сборщик мусора в Java хорошо известен тем, что он не освобождает выделенную память, хотя память больше не требуется. Однако довольно странно, что ваш размер RSS увеличивается до > 3 ГБ, хотя размер вашей кучи ограничен 640 МБ. Используете ли вы какой-либо собственный код в своем приложении или у вас есть собственный пакет оптимизации производительности для Tomcat? В этом случае вы можете, конечно, иметь утечку памяти в своем коде или в Tomcat.
С помощью Java 6u14 компания Sun представила новый сборщик мусора "Garbage-First", который может вернуть память в операционную систему, если она больше не требуется. Он по-прежнему классифицируется как экспериментальный и не включен по умолчанию, но если это возможный вариант для вас, я бы попытался перейти на новейшую версию Java 6 и включить новый сборщик мусора с аргументами командной строки "-XX: + UnlockExperimentalVMOptions - XX: + UseG1GC". Это может решить вашу проблему.