Подтвердить что ты не робот

Почему 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 ГБ.

Этот последний момент был моей "Эврика!" момент.

Мои корректирующие действия:

  1. Рассмотрим обновление до последней версии JRobinLite 1.5.2, которая, по-видимому, лучше
  2. Реализовать правильную обработку ресурсов в базах данных JRobin. На данный момент, когда мое приложение создает базу данных, а затем никогда не выгружает ее после того, как база данных больше не используется активно.
  3. Экспериментируйте с перемещением событий MappedByteBuffer.force() к обновлению базы данных, а не периодическим таймером. Будет ли проблема волшебно уходить?
  4. Непосредственно измените исходный код 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-склон был настолько регулярным? Он не коррелирует с какой-либо из показателей нагрузки приложения.
4b9b3361

Ответ 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". Это может решить вашу проблему.