Использование виртуальной памяти из Java под Linux, слишком много памяти

259

У меня проблема с приложением Java, работающим под Linux.

Когда я запускаю приложение, используя максимальный размер кучи по умолчанию (64 МБ), я вижу, используя приложение tops, что 240 МБ виртуальной памяти выделяются для приложения. Это создает некоторые проблемы с некоторыми другими программами на компьютере, которые относительно ограничены в ресурсах.

Насколько я понимаю, зарезервированная виртуальная память не будет использоваться в любом случае, потому что, как только мы достигаем предела кучи, создается OutOfMemoryErrorисключение. Я запустил одно и то же приложение под Windows и вижу, что размер виртуальной памяти и размер кучи одинаковы.

Есть ли способ настроить виртуальную память для процесса Java под Linux?

Редактировать 1 : проблема не в куче. Проблема в том, что если я установлю, например, кучу 128 МБ, Linux все равно выделит 210 МБ виртуальной памяти, которая никогда не нужна **.

Редактировать 2 : Использование ulimit -vпозволяет ограничить объем виртуальной памяти. Если размер установлен ниже 204 МБ, приложение не будет работать, даже если ему не нужно 204 МБ, только 64 МБ. Поэтому я хочу понять, почему Java требует так много виртуальной памяти. Можно ли это изменить?

Редактировать 3 : в системе работает несколько других приложений, которые встроены. И система имеет ограничение виртуальной памяти (из комментариев, важные детали).

Марио Ортегон
источник
Почему вы обеспокоены использованием виртуальной памяти? Если вы действительно хотите быть обеспокоенным, посмотрите на использование резидентной памяти и прочитайте следующие команды: free, ps, top.
Бассеро
2
В системе работает несколько других приложений, которые встроены. И система имеет ограничение виртуальной памяти.
Марио Ортегон
аааа, дьявол в деталях
basszero
Какую реализацию Java вы используете. IIRC, бесплатное стандартное (не OpenJDK) решение Sun JRE не лицензировано для встроенного использования.
Том Хотин - tackline
Я думаю, что я не использовал «встроенную» часть ... она ограничена в памяти и аппаратное обеспечение настроено, но это все еще стандартный компьютер
Mario Ortegón

Ответы:

630

Это была давняя жалоба на Java, но она в значительной степени бессмысленна и обычно основана на поиске неверной информации. Обычная формулировка выглядит примерно так: «Hello World на Java занимает 10 мегабайт! Зачем это нужно?» Что ж, вот способ заставить Hello World на 64-битной JVM претендовать на 4 гигабайта ... хотя бы одним способом измерения.

java -Xms1024m -Xmx4096m com.example.Hello

Различные способы измерения памяти

В Linux команда top выдает несколько разных чисел для памяти. Вот что говорит пример Hello World:

  PID USER PR NI VIRT RES SHR S% CPU% MEM TIME + КОМАНДА
 2120 кгрег. 20 0 4373 м 15 м 7152 S 0 0,2 0: 00,10 Ява
  • VIRT - это пространство виртуальной памяти: сумма всего на карте виртуальной памяти (см. Ниже). Это в значительной степени бессмысленно, кроме случаев, когда это не так (см. Ниже).
  • RES - это размер резидентного набора: количество страниц, которые в настоящее время находятся в оперативной памяти. Почти во всех случаях это единственное число, которое вы должны использовать, когда говорите «слишком большой». Но это все еще не очень хороший показатель, особенно если говорить о Java.
  • SHR - это объем резидентной памяти, который используется совместно с другими процессами. Для процесса Java это обычно ограничивается общими библиотеками и отображенными в память JAR-файлами. В этом примере у меня был запущен только один процесс Java, поэтому я подозреваю, что 7k - это результат использования библиотек ОС.
  • SWAP не включен по умолчанию и здесь не отображается. Он указывает объем виртуальной памяти, которая в настоящее время находится на диске, независимо от того , находится ли она на самом деле в области подкачки . Операционная система очень хорошо хранит активные страницы в оперативной памяти, и единственные способы ее замены - (1) купить больше памяти или (2) сократить количество процессов, поэтому лучше игнорировать это число.

Ситуация для диспетчера задач Windows немного сложнее. В Windows XP есть столбцы «Использование памяти» и «Размер виртуальной памяти», но в официальной документации ничего не говорится о том, что они означают. Windows Vista и Windows 7 добавляют больше столбцов, и они фактически задокументированы . Из них измерение «Рабочий набор» является наиболее полезным; это примерно соответствует сумме RES и SHR в Linux.

Понимание карты виртуальной памяти

Виртуальная память, используемая процессом, представляет собой сумму всего, что находится в карте памяти процесса. Это включает в себя данные (например, кучу Java), а также все общие библиотеки и файлы отображения памяти, используемые программой. В Linux вы можете использовать команду pmap, чтобы увидеть все объекты, отображенные в пространстве процесса (с этого момента я буду ссылаться только на Linux, потому что это то, что я использую; я уверен, что есть эквивалентные инструменты для Windows). Вот выдержка из карты памяти программы «Hello World»; вся карта памяти имеет длину более 100 строк, и нет ничего необычного в том, чтобы иметь список из тысячи строк.

0000000040000000 36K rx-- /usr/local/java/jdk-1.6-x64/bin/java
0000000040108000 8K rwx-- /usr/local/java/jdk-1.6-x64/bin/java
0000000040eba000 676K rwx-- [anon]
00000006fae00000 21248K rwx-- [anon]
00000006fc2c0000 62720K rwx-- [anon]
0000000700000000 699072K rwx-- [anon]
000000072aab0000 2097152K RWX-- [anon]
00000007aaab0000 349504K rwx-- [anon]
00000007c0000000 1048576K rwx-- [anon]
...
00007fa1ed00d000 1652K r-xs- /usr/local/java/jdk-1.6-x64/jre/lib/rt.jar
...
00007fa1ed1d3000 1024K rwx-- [anon]
00007fa1ed2d3000 4K ----- [anon]
00007fa1ed2d4000 1024K rwx-- [anon]
00007fa1ed3d4000 4K ----- [anon]
...
00007fa1f20d3000 164K rx-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f20fc000 1020K ----- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
00007fa1f21fb000 28K rwx-- /usr/local/java/jdk-1.6-x64/jre/lib/amd64/libjava.so
...
00007fa1f34aa000 1576K rx-- /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3634000 2044K ----- /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3833000 16K rx-- /lib/x86_64-linux-gnu/libc-2.13.so
00007fa1f3837000 4K rwx-- /lib/x86_64-linux-gnu/libc-2.13.so
...

Краткое объяснение формата: каждая строка начинается с адреса виртуальной памяти сегмента. Далее следуют размер сегмента, разрешения и источник сегмента. Этот последний элемент является либо файлом, либо «anon», который указывает блок памяти, выделенный через mmap .

Начиная сверху, мы имеем

  • Загрузчик JVM (т.е. программа, которая запускается при вводе java). Это очень мало; все, что он делает, это загружает в разделяемые библиотеки, где хранится настоящий код JVM.
  • Связка аноновых блоков, содержащих кучу Java и внутренние данные. Это Sun JVM, поэтому куча разбита на несколько поколений, каждое из которых является собственным блоком памяти. Обратите внимание, что JVM выделяет пространство виртуальной памяти на основе -Xmxзначения; это позволяет ему иметь непрерывную кучу. Это -Xmsзначение используется внутри, чтобы сказать, сколько кучи «используется» при запуске программы, и запустить сборку мусора при приближении к этому пределу.
  • Отображаемый в память JAR-файл, в данном случае файл, содержащий «классы JDK». Когда вы отображаете JAR в память, вы можете очень эффективно обращаться к файлам в нем (вместо того, чтобы каждый раз читать его с самого начала). Sun JVM отобразит в памяти все файлы JAR на пути к классам; если вашему приложению необходим код для доступа к JAR, вы также можете отобразить его в памяти.
  • Данные по потокам для двух потоков. Блок 1M - это стек потоков. У меня не было хорошего объяснения для блока 4k, но @ericsoe идентифицировал его как «защитный блок»: у него нет разрешений на чтение / запись, поэтому при обращении к нему будет возникать ошибка сегмента, и JVM отлавливает это и переводит это к StackOverFlowError. Для реального приложения вы увидите десятки, если не сотни этих записей, повторенных через карту памяти.
  • Одна из разделяемых библиотек, которая содержит реальный код JVM. Есть несколько из них.
  • Общая библиотека для стандартной библиотеки C. Это только одна из многих вещей, которые загружает JVM, которые не являются строго частью Java.

Совместно используемые библиотеки особенно интересны: каждая разделяемая библиотека имеет как минимум два сегмента: сегмент только для чтения, содержащий код библиотеки, и сегмент чтения-записи, содержащий глобальные данные о процессах для библиотеки (я не знаю, что сегмент без разрешений есть; я видел его только на x64 Linux). Часть библиотеки, доступная только для чтения, может использоваться всеми процессами, которые используют библиотеку; например, libcимеет 1,5 МБ виртуальной памяти, которую можно использовать совместно.

Когда важен размер виртуальной памяти?

Карта виртуальной памяти содержит много вещей. Некоторые из них доступны только для чтения, некоторые из них являются общими, а некоторые выделяются, но никогда не затрагиваются (например, почти все 4 Гб кучи в этом примере). Но операционная система достаточно умна, чтобы загружать только то, что ей нужно, поэтому размер виртуальной памяти в значительной степени не имеет значения.

Размер виртуальной памяти важен, если вы работаете в 32-битной операционной системе, где вы можете выделить только 2 ГБ (или, в некоторых случаях, 3 ГБ) адресного пространства процесса. В этом случае вы имеете дело с дефицитным ресурсом, и вам, возможно, придется пойти на компромисс, например, уменьшить размер кучи, чтобы отобразить в памяти большой файл или создать много потоков.

Но, учитывая, что 64-битные машины распространены повсеместно, я не думаю, что пройдет много времени, прежде чем объем виртуальной памяти станет абсолютно неактуальной статистикой.

Когда важен размер резидентного набора?

Размер резидентного набора - это та часть виртуальной памяти, которая фактически находится в ОЗУ. Если ваш RSS становится значительной частью вашей общей физической памяти, возможно, пришло время начать беспокоиться. Если ваш RSS-канал начинает занимать всю вашу физическую память, а ваша система начинает обмениваться, уже давно пора начать беспокоиться.

Но RSS также вводит в заблуждение, особенно на слегка загруженной машине. Операционная система не тратит много сил на восстановление страниц, используемых процессом. Это дает мало пользы и может привести к дорогостоящему отказу страницы, если процесс коснется страницы в будущем. В результате статистика RSS может включать в себя множество страниц, которые не используются активно.

Нижняя граница

Если вы не поменялись местами, не слишком переживайте о том, что говорит вам различная статистика памяти. С оговоркой, что постоянно растущая RSS может указывать на какую-то утечку памяти.

С Java-программой гораздо важнее обратить внимание на то, что происходит в куче. Важное значение имеет общий объем потребляемого пространства, и вы можете предпринять некоторые шаги для его уменьшения. Более важным является количество времени, которое вы тратите на сборку мусора, и какие части кучи собираются.

Доступ к диску (т. Е. К базе данных) стоит дорого, а память - дешево. Если вы можете обменять одно на другое, сделайте это.

kdgregory
источник
9
Вы должны принять во внимание, что части памяти, которые в настоящее время заменены, отсутствуют в показателе RES. Таким образом, вы можете иметь низкое значение RES, но только потому, что приложение было неактивно и большая часть кучи была выгружена на диск. Java выполняет очень плохую работу по обмену: на каждом полном GC большая часть кучи проходит и копируется, поэтому, если большая часть вашей кучи была в swap, GC должен загрузить все это обратно в основную память.
Джрудольф
1
Отличный ответ kdgregory! Я бегу во встроенной среде, используя CF, который не имеет пространства подкачки. Поэтому, основываясь на вашем ответе, все мои значения VIRT, SWAP и nFLT взяты из файлов с отображенной памятью ... что теперь имеет смысл мяу. Знаете ли вы, представляет ли значение SWAP страницы, которые еще не были загружены в память, или страницы, которые были выгружены из памяти, или и то, и другое? Как мы можем получить представление о возможном избиении (непрерывная карта затем заменяется)?
Jeach
2
@Jeach - Я был удивлен, что о любом обмене сообщили, поэтому загрузил мой «путешествующий Linux» (флэш-накопитель с Ubuntu 10.04 и без обмена). Когда я включил столбец «SWAP» вверху , я увидел, что у Eclipse было 509 метров. Когда я посмотрел на него с помощью pmap , общее виртуальное пространство составило 650 метров. Поэтому я подозреваю, что цифра «SWAP» представляет все страницы на диске, а не только те, которые не находятся в памяти.
kdgregory
2
Что касается вашего второго вопроса: если вы постоянно читаете страницы с флэш-карты, время ожидания ввода-вывода (показанное в сводке top как «% wa») должно быть высоким. Помните, однако, что это будет высоким для любого действия, особенно записи (при условии, что ваша программа делает любое).
kdgregory
1
> Блок 1M является стеком потоков; Я не знаю, что входит в блок 4K. Блок 4K, который помечен как не имеющий ни прав чтения, ни записи, скорее всего является защитным блоком. При переполнении стека эта область доступна, что вызывает ошибку, которую JVM может затем обработать, создав Java StackOverflowException. Это намного дешевле, чем проверка указателя стека при каждом вызове метода. Охранные зоны без установленных разрешений также могут использоваться в других контекстах.
Эриксо
38

Существует известная проблема с Java и glibc> = 2.10 (включает Ubuntu> = 10.04, RHEL> = 6).

Лекарство заключается в том, чтобы установить это env. переменная:

export MALLOC_ARENA_MAX=4

Если вы работаете с Tomcat, вы можете добавить это в TOMCAT_HOME/bin/setenv.shфайл.

Для Docker, добавьте это в Dockerfile

ENV MALLOC_ARENA_MAX=4

Есть статья IBM о настройке MALLOC_ARENA_MAX https://www.ibm.com/developerworks/community/blogs/kevgrig/entry/linux_glibc_2_10_rhel_6_malloc_may_show_excessive_virtual_memory_usage?lang=en

Это сообщение в блоге говорит

резидентная память, как было известно, ползает способом, подобным утечке памяти или фрагментации памяти.

Существует также открытая ошибка JDK JDK-8193521 «glibc тратит память с конфигурацией по умолчанию»

найдите MALLOC_ARENA_MAX в Google или SO для получения дополнительных ссылок.

Вы можете настроить другие параметры malloc для оптимизации фрагментации выделенной памяти:

# tune glibc memory allocation, optimize for low fragmentation
# limit the number of arenas
export MALLOC_ARENA_MAX=2
# disable dynamic mmap threshold, see M_MMAP_THRESHOLD in "man mallopt"
export MALLOC_MMAP_THRESHOLD_=131072
export MALLOC_TRIM_THRESHOLD_=131072
export MALLOC_TOP_PAD_=131072
export MALLOC_MMAP_MAX_=65536
Лари хотари
источник
Этот ответ действительно помог мне на 64-битном Ubuntu-сервере с сервером TomEE, который получил немного памяти. Ссылка на IBM-статью действительно является глубоким объяснением. Еще раз спасибо за этот хороший совет!
MWiesner
1
JVM может вытекать из собственной памяти, что приводит к аналогичным симптомам. См. Stackoverflow.com/a/35610063/166062 . Незакрытые экземпляры GZIPInputStream и GZIPOutputStream также могут быть источником утечки.
Лари Хотари
3
В Java 8 есть ошибка JVM, которая приводит к неограниченному росту собственной памяти: bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8164293 - если это влияет на вас, использование MALLOC_ARENA_MAXможет замедлить рост памяти, но не решить проблему полностью.
outofcoffee
@LariHotari очень ценит ваши усилия по указанию версии glibc и redhat
Сэм,
2
Java 8u131 содержит багпортированный багпикс для связанной ошибки JVM JDK-8164293 bugs.openjdk.java.net/browse/JDK-8178124 .
Лари Хотари
9

Объем памяти, выделенный для процесса Java, в значительной степени соответствует ожидаемому. У меня были похожие проблемы с запуском Java во встроенных системах / системах с ограниченным объемом памяти. Запуск любого приложения с произвольными ограничениями виртуальных машин или в системах, в которых не хватает достаточного количества подкачки, может привести к сбою. Кажется, это характерная черта многих современных приложений, которые не предназначены для использования в системах с ограниченными ресурсами.

У вас есть еще несколько вариантов, которые вы можете попробовать и ограничить объем памяти вашей JVM. Это может уменьшить объем виртуальной памяти:

-XX: ReservedCodeCacheSize = 32m Зарезервированный размер кода (в байтах) - максимальный размер кода. [Solaris 64-bit, amd64 и -server x86: 48m; в 1.5.0_06 и более ранних версиях, Solaris 64-bit и and64: 1024m.]

-XX: MaxPermSize = 64 м Размер постоянного поколения. [5.0 и новее: 64-битные виртуальные машины масштабируются на 30% больше; 1,4 драм: 96 м; 1.3.1 -клиент: 32м.]

Кроме того, вы также должны установить для -Xmx (максимальный размер кучи) значение, максимально приближенное к фактическому пиковому использованию памяти вашим приложением. Я считаю, что по умолчанию JVM по-прежнему удваивает размер кучи каждый раз, когда он расширяет его до максимума. Если вы начнете с кучи 32M, а ваше приложение достигнет пика 65M, то в итоге размер кучи увеличится до 32M -> 64M -> 128M.

Вы также можете попробовать это сделать виртуальную машину менее агрессивной в отношении роста кучи:

-XX: MinHeapFreeRatio = 40 Минимальный процент свободного кучи после GC, чтобы избежать расширения.

Кроме того, насколько я помню из экспериментов с этим несколько лет назад, количество загруженных нативных библиотек оказало огромное влияние на минимальную площадь. Загрузка java.net.Socket добавлена ​​более 15M, если я правильно помню (и я, вероятно, не).

Джеймс Шек
источник
7

Для Sun JVM требуется много памяти для HotSpot, и он отображается в библиотеках времени выполнения в общей памяти.

Если проблема с памятью, рассмотрите возможность использования другой JVM, подходящей для встраивания. У IBM есть j9, и есть open source "jamvm", который использует библиотеки путей к классам GNU. Также у Sun есть Squeak JVM, работающая на SunSPOTS, так что есть альтернативы.

Турбьерн Равн Андерсен
источник
Это опция для отключения горячей точки?
Марио Ортегон,
Может быть. Проверьте параметры командной строки для используемой вами JVM.
Торбьерн Равн Андерсен
3

Просто подумал, но вы можете проверить влияние на ulimit -vопции .

Это нереальное решение, поскольку оно ограничит адресное пространство, доступное для всех процессов, но это позволит вам проверить поведение вашего приложения с ограниченной виртуальной памятью.

VonC
источник
Это именно то, что моя проблема. Моя куча установлена ​​на 64M, но Linux оставляет 204MB. Если установить значение ulimit ниже 204, приложение вообще не запустится.
Марио Ортегон
Интересно: установка ulimit может иметь непреднамеренный побочный эффект для других процессов, объясняя, почему приложение не может работать.
VonC
Кажется, проблема в том, что Java требует зарезервировать этот больший объем виртуальной памяти, даже если он ее не использует. В окнах используемая виртуальная память и настройка Xmx довольно близки.
Марио Ортегон,
Вы пробовали это с JRockit JVM?
VonC
Поскольку выделение памяти JVM представляет собой сумму распределения кучи и размера Perm (первое можно исправить с помощью параметров -Xms и -Xmx), пробовали ли вы некоторые параметры с помощью -XX: PermSize и -XX: MaxPermSize (по умолчанию от 32 МБ до 64 МБ в зависимости от версии JVM)?
VonC
3

Одним из способов уменьшения кучи системы с ограниченными ресурсами может быть использование переменной -XX: MaxHeapFreeRatio. Обычно это значение равно 70, и это максимальный процент кучи, который свободен до того, как GC сжимает его. Если установить более низкое значение, вы увидите, например, в профилировщике jvisualvm, что для вашей программы обычно используется меньший размер кучи.

РЕДАКТИРОВАТЬ: Чтобы установить небольшие значения для -XX: MaxHeapFreeRatio, вы также должны установить -XX: MinHeapFreeRatio Например

java -XX:MinHeapFreeRatio=10 -XX:MaxHeapFreeRatio=25 HelloWorld

EDIT2: добавлен пример для реального приложения, которое запускается и выполняет ту же задачу, одно с параметрами по умолчанию, а другое с 10 и 25 в качестве параметров. Я не заметил какой-либо реальной разницы в скорости, хотя в теории Java должен использовать больше времени для увеличения кучи в последнем примере.

Параметры по умолчанию

В итоге максимальная куча составляет 905, использованная куча - 378

MinHeap 10, MaxHeap 25

В итоге максимальная куча составляет 722, использованная куча - 378

Это на самом деле имеет некоторый недостаток, так как наше приложение работает на сервере удаленного рабочего стола, и многие пользователи могут запускать его одновременно.

runholen
источник
1

Sun java 1.4 имеет следующие аргументы для управления объемом памяти:

-Xmsn Укажите начальный размер пула выделения памяти в байтах. Это значение должно быть кратно 1024 больше 1 МБ. Добавьте букву k или K для обозначения килобайт, или m или M для обозначения мегабайт. Значение по умолчанию составляет 2 МБ. Примеры:

           -Xms6291456
           -Xms6144k
           -Xms6m

-Xmxn Укажите максимальный размер в байтах пула выделения памяти. Это значение должно быть кратно 1024 больше 2 МБ. Добавьте букву k или K для обозначения килобайт, или m или M для обозначения мегабайт. Значение по умолчанию составляет 64 МБ. Примеры:

           -Xmx83886080
           -Xmx81920k
           -Xmx80m

http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/java.html

Java 5 и 6 есть еще. См. Http://java.sun.com/javase/technologies/hotspot/vmoptions.jsp

Пол Томблин
источник
1
У меня проблема не с размером кучи, а с объемом виртуальной памяти, назначаемой Linux
Mario Ortegón
Прочитайте объяснение kdgregory. Уменьшение размера кучи, «Новый размер» и другие настраиваемые параметры уменьшат объем РЕАЛЬНОЙ памяти, которую занимает jvm.
Пол Томблин
У него может быть законная проблема. Некоторые приложения (например, одно, которое я написал) отображают файл размером 1 ГБ, а некоторые системы имеют только 2 ГБ виртуальной памяти, некоторые из которых заполняются общими библиотеками. И если это проблема, он должен обязательно отключить рандомизацию DSO. Есть опция в / proc.
Zan Lynx
0

Нет, вы не можете настроить объем памяти, необходимый для виртуальной машины. Тем не менее, обратите внимание, что это виртуальная память, а не резидентная, поэтому она остается без вреда, если не используется на самом деле.

С другой стороны, вы можете попробовать другую JVM, чем Sun, с меньшим объемом памяти, но я не могу советовать здесь.

Marko
источник