Почему java ест много памяти

Обновлено: 05.07.2024

Взял тестовый сорс из книги, немного его подрихтовал на запуск 1000 тредов. Запустил, меряю ps_mem -p pid и вижу, что потихонечку память процесса увеличивается. Это нормально или где-то тут протекает? :)

1 тред это один стек, который по дефолту кажись 1 метр, 500 тредов = 500 метров.

По коду, если я правильно понял, у тебя каждый тред живет 2.5 секунды (5 итераций цикла по 500 мс). Т.е. после захвата 1 гига на стеки (в течении

5 секунд после старта приложения) вроде ничего больше создаваться не будет, потому дальше гига память расти вроде не должна. Вообще за памятью в jvm лучше следить через jvisualvm (может уже по другому называется).

Aber ★★★★ ( 11.04.20 22:41:21 )
Последнее исправление: Aber 11.04.20 22:50:58 (всего исправлений: 1)

-Xmx то задал? Без него jvm сожрет все.

У тебя 1000-ядерная система?

Begemoth ★★★★★ ( 11.04.20 22:49:38 )
Последнее исправление: Begemoth 11.04.20 22:50:21 (всего исправлений: 1)

Вполне нормально. Запускать так много нитей на одной виртуальной машине не так чтобы рекомендуется. А главное - зачем?

Зачем? Они в основном спят, в таком случае планировщик ОС нормально все разруливает.

Ну это сейчас они спят, что там ТС учудить хочет - отдельный вопрос.


Не забывай, что stdout тоже не бесплатный.

А какая разница?


Что за книжка такая странная?

Я не эксперт в джаве, но:

Проще написать так:


А какой прок с 1000 потоков, если нет?

Вопрос был не про количество потоков, а их связь с количеством ядер. За исключением расспаралеливания матана, количество потоков и количество ядер ни как не связаны. Прямо сейчас у меня линукс держит 500 запущенных потоков, и ничего.

RazrFalcon ★★★★★ ( 11.04.20 23:47:13 )
Последнее исправление: RazrFalcon 11.04.20 23:47:35 (всего исправлений: 1)


Да хоть миллион, что ты в рамках одной программы с этими потоками собрался делать, если не cpu-bound задачи параллелить?

Цикл без условия живет вечно


руки за такое оторвать.

Да что угодно. Особенно если мы говорим о GUI/игры, где главный поток не должен быть занят и всё вынесено в доп. потоки.

PS: я как-то видел как dropbox запускал 400 потоков на маке. Так что…

Ну тогда 1 гиг на стеки почти сразу и дальше память расти не должна, сколько по факту у тебя занято?

стартует со 100мб и растет. Вот это и подозрительно. По идее должна была создать стек и быть статичной т.к внутри никаких объектов не создается

bryak ★★★★ ( 12.04.20 00:38:36 )
Последнее исправление: bryak 12.04.20 00:39:22 (всего исправлений: 1)


Он там стринги в циклах создаёт. Не из-за этого ли?

Я хотел проверить гипотезу, но какая же многословная гадость эта ваша джава, куча бойлерплейтинга даже для самых элементарных вещей типа из /proc/self/statm почитать, плюнул.


И что там создается при использлвании System.out.println() ?

Он там стринги в циклах создаёт. Не из-за этого ли?

Да, так и есть, конкатенация порождает новую строку.


GC их не собирает?

Собирает когда захочет, главное что 1к строк генерируется каждые пол секунды, это 1к объектов, правда они мелкие, из-за них с такими темпами едва будет сильно больше мегабайта в секунду, ну и GC должен периодически удалять мусор, так что хип должен перестать расти через несколько секунд. Это мои предложения, код не запускал. Я вот только не знаю почему приложение сразу не захватило 1 гиг на стеки для 1к тредов, видно я что-то не так себе представляю.


Тот мусор, что он печатает ¯\_(ツ)_/¯

Вероятность того, что код запускается из IDE, примерно равна единице. И тут начинают действовать побочные эффекты плюшек IDEшной консольки - например, такие - которые из-за использования непонятно чего для замера потребления памяти вполне могут влиять на результат.


Так чего с Xmx? Если не задавал, то у тебя и один поток будет расти. Запусти с -Xmx16m и смотри. Думаю, будет нормально.

Legioner ★★★★★ ( 12.04.20 02:30:26 )
Последнее исправление: Legioner 12.04.20 02:30:43 (всего исправлений: 1)


ТС троллит, очевидно же. У него нет предельного значения и графика

Всем спасибо. Утечка решилась путем комментирования принтов. Хотел посмотреть, сколько съедает java оперативной памяти при создании тредов


Утечка решилась путем комментирования принтов.

Утечка решилась путем комментирования поста на ЛОР.


Которой не было.

cdshines ★★★★ ( 12.04.20 03:07:06 )
Последнее исправление: cdshines 12.04.20 03:08:48 (всего исправлений: 1)

1 тред это один стек, который по дефолту кажись 1 метр, 500 тредов = 500 метров.

Я вот только не знаю почему приложение сразу не захватило 1 гиг на стеки для 1к тредов, видно я что-то не так себе представляю.

Стек используется по мере надобности. А не в момент создания. Задаётся лишь максимальный размер стека, и память не резервируется. И да, на Linux размер стека 8мб.

И на Windows точно также. Только размер стека по умолчанию 1мб.

А не в момент создания. Задаётся лишь максимальный размер стека

Спасибо, не знал. Помнил из времен студенческих лабораторных на сях, что в зависимости от модели памяти на x86 стек занимал либо пол сегмента, либо ему отдавался весь сегмент, так и запомнилось что он вроде как статически аллокировался :)


Врешь по всем пунктам, кроме одного, но и там не совсем правда (речь о хотспоте, потому что у автора наверняка хотспот).

Стек используется по мере надобности. А не в момент создания.

Each Java Virtual Machine thread has a private Java Virtual Machine stack, created at the same time as the thread.

Он может расти и сжиматься, но ниже xss не будет (в хотспоте точно).

Задаётся лишь максимальный размер стека

Xss - это минимальный размер.

И да, на Linux размер стека 8мб.

Он может расти и сжиматься, но ниже xss не будет (в хотспоте точно).

Но если бы он мог расти динамически то не было бы Stack Overflow Exception, а был был бы только OOM. Эх, неужели мне придется гуглить доку, я надеялся на лоре все расскажут.

Хз, как прокоментируешь скриншоты?

Даже Java не ест один мегабайт под поток, а примерно 57 000 байт..

fsb4000 ★★★★★ ( 12.04.20 13:01:04 )
Последнее исправление: fsb4000 12.04.20 13:24:24 (всего исправлений: 1)


Я тебе уже дал ссылку на документ (jvms 2.5.2). Еще цитата:

This specification permits Java Virtual Machine stacks either to be of a fixed size or to dynamically expand and contract as required by the computation. If the Java Virtual Machine stacks are of a fixed size, the size of each Java Virtual Machine stack may be chosen independently when that stack is created.



да хоть на BSD фак, физической памяти потоки во всех вменяемых OS используют лишь сколько нужно, а сколько выделено виртуальной памяти никого не волнует.

Ну вот тебе скрин из WSL того же теста создания 10000 потоков:

Важен лишь столбец RES…


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

64-bit Linux allows up to 128 TB of virtual address space for individual processes

Виртуальной памяти у нас можно считать неограниченно, важно лишь реальное потребление оперативки.

Даже в Windows у нас есть 8 TB для каждого процесса.

Виртуальная память со времён создания X86-64 перестала быть ресурсом.


Пробовал когда-нибудь дождаться gc в машине с засвопленной памятью? Прекрати уже говорить глупости и почитай матчасть, и перестань упрощать все до своих игрушечных примеров. Зачем умножать безграмотность? Все, что ты написал, потом кто-нибудь прочитает и наберется этой ерунды.


Вообще за памятью в jvm лучше следить через jvisualvm (может уже по другому называется).

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

Вот именно, не умножай безграмотность, а иди и изучай.

Виртуальная память связана со свопом примерно никак.

Виртульная память - просто какие-то диапазоны адресов резервируются для дальнейшего использования. Никакого маппинга на реальную оперативную память или своп не происходит.

Ах, как у нас процесс отьъел 43GB виртуальной памяти, если у нас есть лишь 32GB реальной + 5GB свопа? Да ещё и показывается, что большинство памяти свободно. Картина мира cdshines разрушена, что поделать, это жизнь…


При чем здесь это? Еще раз повторяю, jvm, которой ты выделил «неважно сколько много» виртуальной памяти, работать будет не просто плохо, а настолько отвратительно, что прямо невозможно. Это, конечно, если запускать настоящие приложения, а не игрушечные примеры, написанные с целью поддержать свое бестолковое мнение.

Вот ещё вспомнилось что я знаю про виртуальную память:

С++ программы с включенным AddressSanitizer потребляют дохрена виртуальной памяти:

The ulimit -v command makes little sense with ASan-ified binaries because ASan consumes 20 terabytes of virtual memory (plus a bit).

Так что Virtual memory != Ram + Swap.


На реальном устройстве:

Одноплатник с 1 гигом памяти спокойно переваривает процессы с выделенными 32 терабайтами виртуальной памяти.

А у тебя нет? Вот ты лох.

Ради интереса попробовал данный пример.
При запуске потребление памяти (top) примерно 150 MB, visualvm показывал <120MB.
Чем дольше работает, тем потребление ниже (через 15 минут): visualvm показывает <30 MB, а top –

74 MB.
Пи этом всё время работы программы – потоков 1010 штук (они не завершаются, т.к. цикл бесконечный).

VM – OpenJDK 1.8.
Запуск производился в среде Eclipse.

Если запускать в консоли с использованием OpenJDK 11, то потребление top – 155 MB, visualvm – 120 MB. Потребление посоянное (видать для этого примера в восьмой версии JIT работет лучше, чем в 11-ой).

Хотелось бы у автора узнать подробности – как запускался пример?

Года два назад при использовании Eclipse в связке с GTK3 потребление виртуальной памяти доходило до 9 TB (террабайт).
При этом опреативы было 32 GB, а swap – 64 GB. А вот резидентной памяти потреблялось в соответствии с Xmx – 2 GB.
При этом то же самый Eclipse, на той же машине, но с использванием GTK2 уже не был таким прожорливым в плане виртуальной памяти – хватало нескольких GB, при тех же 2GB резидентной.

Да и не должен вроде, они должны кэшироваться в стринг пуле. Скорее всего поэтому и память отъедалась.

Для моего приложения память, используемая процессом Java, намного больше, чем размер кучи.

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

Размер кучи установлен на 128 МБ ( -Xmx128m -Xms128m ), в то время как контейнер занимает до 1 ГБ памяти. При нормальных условиях требуется 500 МБ. Если у док-контейнера есть предел ниже (например, mem_limit=mem_limit=400MB ), процесс уничтожается из-за нехватки памяти в ОС.

Не могли бы вы объяснить, почему процесс Java использует намного больше памяти, чем куча? Как правильно определить размер памяти Docker? Есть ли способ уменьшить объем памяти, занимаемой вне кучи процесса Java?

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

Из хост-системы я получаю память, используемую контейнером.

Изнутри контейнера я получаю память, используемую процессом.

Приложение представляет собой веб-сервер, использующий Jetty / Jersey / CDI, встроенный в большой далекий 36 МБ.

Используются следующие версии ОС и Java (внутри контейнера). Образ Docker основан на openjdk:11-jre-slim .

Виртуальная память, используемая процессом Java, выходит далеко за рамки Java Heap. Вы знаете, JVM включает в себя множество подсистем: сборщик мусора, загрузка классов, JIT-компиляторы и т. Д., И все эти подсистемы требуют определенного объема оперативной памяти для функционирования.

JVM не единственный потребитель оперативной памяти. Собственные библиотеки (включая стандартную библиотеку классов Java) также могут выделять собственную память. И это не будет даже видно для Native Memory Tracking. Само Java-приложение также может использовать память вне кучи с помощью прямых байтовых буферов.

Так что же занимает память в процессе Java?

Части JVM (в основном показываются Native Memory Tracking)

Самая очевидная часть. Здесь живут объекты Java. Куча занимает до -Xmx объема памяти.

Структуры и алгоритмы GC требуют дополнительной памяти для управления кучей. Это структуры Mark Bitmap, Mark Stack (для обхода графа объекта), Remembered Sets (для записи межрегиональных ссылок) и другие. Некоторые из них могут быть настроены напрямую, например, -XX:MarkStackSizeMax , другие зависят от компоновки кучи, например, чем больше области G1 ( -XX:G1HeapRegionSize ), тем меньше запоминаемые множества.

Загрузка памяти GC варьируется между алгоритмами GC. -XX:+UseSerialGC и -XX:+UseShenandoahGC имеют наименьшие накладные расходы. G1 или CMS могут легко использовать около 10% от общего размера кучи.

Содержит динамически сгенерированный код: JIT-скомпилированные методы, интерпретатор и заглушки во время выполнения. Его размер ограничен -XX:ReservedCodeCacheSize (по умолчанию 240M). Отключите -XX:-TieredCompilation , чтобы уменьшить объем скомпилированного кода и, следовательно, использование кэша кода.

Сам JIT-компилятор также требует памяти для своей работы. Это можно снова уменьшить, отключив многоуровневую компиляцию или уменьшив количество потоков компилятора: -XX:CICompilerCount .

Метаданные класса (байт-коды методов, символы, пулы констант, аннотации и т. Д.) Хранятся в области вне кучи, называемой Metaspace. Чем больше классов загружено - тем больше метапространства используется. Общее использование может быть ограничено -XX:MaxMetaspaceSize (по умолчанию не ограничено) и -XX:CompressedClassSpaceSize (1G по умолчанию).

Две основные хеш-таблицы JVM: таблица символов содержит имена, подписи, идентификаторы и т. Д., А таблица String содержит ссылки на интернированные строки. Если отслеживание собственной памяти указывает на значительное использование памяти таблицей строк, это, вероятно, означает, что приложение чрезмерно вызывает String.intern .

Стеки потоков также отвечают за использование оперативной памяти. Размер стека контролируется -Xss . По умолчанию 1М на поток, но, к счастью, все не так плохо. ОС распределяет страницы памяти лениво, т. Е. При первом использовании, поэтому фактическое использование памяти будет намного ниже (обычно 80-200 КБ на стек потоков). Я написал скрипт, чтобы оценить, сколько RSS принадлежит стекам потоков Java.

Существуют и другие части JVM, которые выделяют собственную память, но обычно они не играют большой роли в общем потреблении памяти.

Прямые буферы

Приложение может явно запросить память вне кучи, вызвав ByteBuffer.allocateDirect . По умолчанию предел нехватки кучи равен -Xmx , но его можно переопределить с помощью -XX:MaxDirectMemorySize . Прямые байтовые буферы включены в раздел Other вывода NMT (или Internal до JDK 11).

Объем используемой прямой памяти виден через JMX, например, в JConsole или Java Mission Control:

BufferPool MBean

Помимо прямых ByteBuffers могут быть MappedByteBuffers - файлы, сопоставленные с виртуальной памятью процесса. NMT не отслеживает их, однако MappedByteBuffers также может занимать физическую память. И нет простого способа ограничить, сколько они могут взять. Вы можете просто увидеть фактическое использование, посмотрев карту памяти процесса: pmap -x <pid>

Родные библиотеки

Код JNI, загруженный System.loadLibrary , может выделить столько памяти вне кучи, сколько ему нужно, без контроля со стороны JVM. Это также касается стандартной библиотеки классов Java. В частности, незакрытые ресурсы Java могут стать источником утечки памяти. Типичными примерами являются ZipInputStream или DirectoryStream .

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

В этом ответе описывается, как профилировать распределение собственной памяти с помощью async-profiler.

Проблемы с распределителем

Процесс обычно запрашивает собственную память либо непосредственно из ОС (с помощью mmap системного вызова), либо с помощью malloc - стандартного распределителя libc. В свою очередь, malloc запрашивает большие порции памяти из ОС, используя mmap , а затем управляет этими порциями в соответствии со своим собственным алгоритмом выделения. Проблема в том, что этот алгоритм может привести к фрагментации и jemalloc , альтернативный распределитель, часто выглядит умнее обычного libc malloc , поэтому переключается на < > может привести к уменьшению занимаемой площади бесплатно.

Вывод

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

Можно уменьшить или ограничить определенные области памяти (например, кэш кода) с помощью флагов JVM, но многие другие вообще не контролируются JVM.

Один из возможных подходов к настройке пределов Docker - наблюдать за фактическим использованием памяти в «нормальном» состоянии процесса. Существуют инструменты и методы для исследования проблем с использованием памяти Java: Отслеживание встроенной памяти, pmap, jemalloc, Объем памяти, занимаемой процессом Java.

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

Почему при указании -Xmx = 1g моя JVM использует больше памяти, чем 1 ГБ памяти?

Указание -Xmx = 1g указывает JVM выделить кучу в 1 Гб. Он не говорит JVM ограничить использование всей памяти до 1 ГБ. Существуют таблицы карточек, кеши кода и всякие другие структуры данных без кучи. Параметр, который вы используете для указания общего использования памяти: -XX: MaxRAM. Имейте в виду, что с -XX: MaxRam = 500 м ваша куча будет примерно 250 МБ.

Java видит размер памяти хоста и не знает никаких ограничений памяти контейнера. Это не создает нагрузку на память, поэтому GC также не нужно освобождать использованную память. Я надеюсь, что XX:MaxRAM поможет вам уменьшить объем памяти. В конце концов, вы можете настроить конфигурацию GC ( -XX:MinHeapFreeRatio , -XX:MaxHeapFreeRatio , . )

Существует много типов метрик памяти. Похоже, что Docker сообщает об объеме памяти RSS, который может отличаться от «выделенной» памяти, сообщаемой jcmd (более ранние версии Docker сообщают о том, что RSS + кэш используется как память). Хорошее обсуждение и ссылки: Разница между резидентами Задать размер (RSS) и общую выделенную память Java (NMT) для JVM, работающей в контейнере Docker

(RSS) память может быть съедена также некоторыми другими утилитами в контейнере - оболочкой, менеджером процессов, . Мы не знаем, что еще выполняется в контейнере и как вы запускаете процессы в контейнере.

TL ; DR

Подробное использование памяти обеспечивается деталями Native Memory Tracking (NMT) (главным образом метаданные кода и сборщик мусора). В дополнение к этому компилятор Java и оптимизатор C1 / C2 потребляют память, не указанную в сводке.

Объем памяти может быть уменьшен с помощью флагов JVM (но есть последствия).

Определение размеров контейнера Docker должно выполняться путем тестирования с ожидаемой загрузкой приложения.

Деталь для каждого компонента

Общее пространство классов можно отключить внутри контейнера, поскольку классы не будут использоваться другим процессом JVM. Можно использовать следующий флаг. Это удалит общее пространство классов (17 МБ).

Серийный номер сборщика мусора занимает минимальный объем памяти за счет увеличения времени паузы при обработке сбора мусора (см. Сравнение Алексея Шипилева между GC на одном снимке ). Это может быть включено с помощью следующего флага. Это может сэкономить до используемого пространства GC (48 МБ).

Компилятор C2 можно отключить с помощью следующего флага, чтобы уменьшить данные профилирования, используемые для принятия решения о том, оптимизировать или нет метод.

Кодовое пространство уменьшается на 20 МБ. Кроме того, память вне JVM уменьшается на 80 МБ (разница между пространством NMT и пространством RSS). Оптимизирующему компилятору C2 требуется 100 МБ.

Компиляторы C1 и C2 можно отключить с помощью следующего флага.

Объем памяти вне JVM теперь меньше общего выделенного пространства. Кодовое пространство уменьшается на 43 МБ. Осторожно, это сильно влияет на производительность приложения. Отключение компилятора C1 и C2 уменьшает используемую память на 170 МБ.

Использование Компилятора виртуальной машины Graal (замена C2) приводит к небольшому увеличению меньший объем памяти. Это увеличивает объем памяти кода на 20 МБ и уменьшает объем внешней памяти JVM на 60 МБ.

Java требует много памяти. Сама JVM требует много памяти для запуска. Куча - это память, доступная внутри виртуальной машины, доступная вашему приложению. Поскольку JVM - это большой пакет, в котором есть все возможные вкусности, для загрузки требуется много памяти.

Начиная с java 9 у вас есть что-то под названием проект Jigsaw, которое может уменьшить объем используемой памяти, когда вы Запустите Java-приложение (вместе со временем запуска). Головоломка проекта и новая система модулей не обязательно создавались для уменьшения необходимой памяти, но если это важно, вы можете попробовать.

Если вы не хотите тратить время на это, вы можете просто выделить больше памяти. Иногда это лучшее.

Как правильно определить ограничение памяти Docker? Проверьте приложение, отслеживая его в течение некоторого времени. Чтобы ограничить память контейнера, попробуйте использовать опцию -m, --memory bytes для команды docker run - или что-то эквивалентное, если вы запускаете его иначе подобно

Основные симптомы утечек памяти Java

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

Ошибки конфигурации выглядящие как утечки памяти

Перед тем, как заглянете в ситуации вызывающие проблемы с памятью Java и проведете анализ, необходимо убедиться, что исследования не имеют отношения к абсолютно другой задаче. Часть ошибок out-of-memory возникают из-за различных ошибок, например ошибок конфигурации. У приложения, возможно, недостаток памяти в куче или оно конфликтует в системе с другими приложениями. Если начинаете говорить о проблемах нехватки памяти, но не можете определить что вызывает утечку, взгляните на приложение по-другому. Обнаружится, что нужно сделать изменения в потоке финализации или увеличить объем permanent generation пространства, являющегося областью памяти JVM для хранения описания классов Java и некоторых дополнительных данных.

Преимущества инструментов мониторинга памяти

Инструменты мониторинга памяти дают бОльшую видимость использования доступных ресурсов приложением Java. Используя данное ПО, вы делаете шаг для сужения поиска корня проблемы утечки памяти и прочих инцидентов связанных с производительностью. Инструменты идут в нескольких категориях, и вам, возможно, нужно использовать множество приложений, чтобы разобраться как начать правильно обозначать проблему и что пошло не так, даже если вы имеете дело с утечками памяти. Heap dump (дампа кучи) файлы дают необходимые сведения для анализа Java-памяти. В этом случае вам нужно использовать два инструмента: один для генерации дамп-файла и другой для подробного анализа. Такое решение дает детализированную информацию о том, что происходит с приложением. Один раз инструмент указывает места возможных проблем и работает над сужением площади, чтобы обнаружить точное место возникновения инцидента. И этот период времени - время самой длинной и портящей настроение части проб и ошибок. Анализатор памяти указывает несколько проблем в коде, но вы не уверены абсолютно, с какими проблемами столкнулось ваше приложение. Если всё ещё сталкиваетесь с прежней ошибкой, начните сначала и поработайте над другой возможной ошибкой. Сделайте одно изменение за раз и попытайтесь продублировать ошибку. Нужно будет дать приложению поработать некоторое время, чтобы продублировать условия возникновения ошибки. Если при первом тесте происходит утечка памяти, не забудьте протестировать приложение под нагрузкой. Приложение может работать отлично с небольшим количеством данных, но может снова выбросить прежние ошибки при работе с большим объемом данных. Если еще возникает всё та же самая ошибка, нужно начать сначала и разобрать другую возможную причину. Инструменты мониторинга памяти доказывают свою пользу после того, когда приложение стало полностью работающим. Можно удаленно наблюдать за производительностью JVM и проактивным обнаружением сбойных ситуаций перед тем, как разработчик погрузится в проблему и будет собирать исторические данные производительности, чтобы помочь себе в будущем улучшить техники программирования и наблюдать как Java работает под тяжелой нагрузкой. Многие решения включают режимы оповещения "опасность" или другие подобные режимы и разработчик сразу может знать, что происходит не так, как хотелось. Каждый разработчик не хочет, чтобы критическое приложение, будучи в промэксплуатации, падало и являлось причиной потери десятков или сотен тысяч долларов во время простоя приложения, поэтому инструменты мониторинга памяти уменьшают время реагирования разработчика. Приложения мониторинга памяти дают начать процесс диагностики мгновенно, вместо того, чтобы попросить вас пойти к заказчику, где никто не скажет какая именно ошибка случилось или какой код ошибки выдало приложение. Если часто погружаетесь в проблемы памяти и производительности вашего Java-приложения, плотно возьмитесь за процесс тестирования. Обозначьте каждую слабую область в процессе разработки и измените стратегии тестирования. Посоветуйтесь с коллегами и сравните свои подходы тестирования с существующими лучшими практиками. Иногда вам надо пересмотреть маленький фрагмент кода и далее обеспечить длительное воздействие на все приложение.

Роль Garbage Collector на память Java и утечки памяти

Garbage Collector (cборщик мусора) в Java играет ключевую роль в производительности приложения и использования памяти. Он ищет неиспользуемые (мертвые) объекты и удаляет их. Эти объекты больше не занимают память, так что ваше приложение продолжает обеспечивать доступность ресурсов. Иногда приложение не дает GC достаточно времени или ресурсов для удаления мертвых объектов и они накапливаются. Можно столкнуться с такой ситуацией когда идет активное обращение к объектам, которые, вы полагаете, мертвы. Сборщик мусора не может сделать ничего c этим, т.к. его механизм автоматизированного управления памяти обходит активные объекты. Обычно сборщик мусора работает автономно, но необходимо настроить его поведение на реагирование тяжелых проблем с памятью. Однако, GC может сам приводить к проблемам производительности.

Области GC

Сборщик мусора для оптимизации сборки разделяет объекты на разные области. В Young Generation представлены объекты, которые отмирают быстро. Сборщик мусора часто работает в этой области, с того момента, когда он должен проводить очистку. Объекты оставшиеся живыми по достижению определенного периода переходят в Old Generation. В области Old Generation объекты остаются долгое время, и они не удаляются сборщиком так часто. Однако, когда сборщик работает в области, приложение проходит проходит через большую операцию, где сборщик смотрит сквозь живые объекты для очистки мусора. В итоге объекты приложения находятся в конечной области permanent generation. Обычно, эти объекты включают нужные метаданные JVM. Приложение не генерирует много мусора в Permanent Generation, но нуждается в сборщике для удаления классов когда классы больше не нужны.

Связь между Garbage Collector и временем отклика

Сборщик мусора, независимо от приоритета исполнения потоков приложения, останавливает их не дожидаясь завершения. Такое явление называется событием "Stop the World". Область Young Generation сборщика мусора незначительно влияет на производительность, но проблемы заметны, если GC выполняет интенсивную очистку. В конечном итоге вы оказываетесь в ситуации, когда минорная сборка мусора Young Generation постоянно запущена или Old Generation переходит в неконтролируемое состояние. В такой ситуации нужно сбалансировать частоту Young Generation с производительностью, которая требует увеличение размера этой области сборщика. Области Permanent Generation и Old Generation сборщика мусора значительно влияют на производительность приложения и использования памяти. Эта операция major очистки мусора проходит сквозь heap, чтобы вытолкнуть отмершие объекты. Процесс длится дольше чем minor сборка и влияние на производительность может идти дольше. Когда высокая интенсивность очистки и большой размер области Old Generation, производительность всего приложения увязывает из-за событий "Stop the world". Оптимизация сборки мусора требует мониторинга как часто программа запущена, влияния на всю производительность и способов настройки параметров приложения для уменьшения частоты мониторинга. Возможно нужно будет идентифицировать один и тот же объект, размещенный больше, чем один раз, причем приложению не нужно отгораживаться от размещения или вам надо найти точки сжатия, сдерживающие всю систему. Получение правильного баланса требует уделения близкого внимания ко всему от нагрузки на CPU до циклов вашего сборщика мусора, особенно если Young и Old Generation несбалансированы. Адресация утечек памяти и оптимизация сборки мусора помогает увеличить производительность Java-приложения. Вы буквально жонглируете множеством движущихся частей. Но с правильным подходом устранения проблем и инструментами анализа, спроектированных чтобы дать строгую видимость, вы достигнете света в конце туннеля. В противном случае замучаетесь с возникающими неполадками связанных с произодительностью. Тщательное размещение памяти и её мониторинг играют критическую роль в Java-приложении. Необходимо полностью взять в свои руки взаимодействие между сборкой мусора, удалением объектов и производительностью, чтобы оптимизировать приложение и избежать ошибок упирающихся в нехватку памяти. Инструменты мониторинга дают оставаться на высоте, чтобы обнаружить возможные проблемы и обозначить тенденции утилизации памяти так, что вы принимаете проактивный подход к исправлению неисправностей. Утечки памяти часто показывают неэффективность устранения неисправностей обычным путем, особенно если вы сталкиваетесь с неверными значениями параметров конфигурации, но решения вопросов связанных с памятью помогают быстро избежать инцидентов стоящих у вас на пути. Совершенство настройки памяти Java и GC делают ваш процесс разработки намного легче.

На сервере запустил веб-приложение на Java и Tomcat с подключением к MySQL. Для ограничения памяти задал следующие опции:

Согласно моим ожиданиям, приложении должно занимать в памяти 104Мб (с учётом 64 потоков, реально меньше).

Но после запуска java-процесс занимает примерно 200Мб (судя по полю RES утилиты top). Через несколько часов ничего неделания, приложение уже занимает больше 300Мб.

На что расходуются эти 200Мб памяти и можно ли их как-то ограничить средствами Java?

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

Обновление

Снял дамп, после нескольких минут активной работы с приложением. При снятии дампа процесс java занимал 300Мб памяти, согласно статистике Tomcat'а использовано было всего 64Мб (Heap + PermGen + CodeCache). Открыл дамп в jVisualVM и там указано "Total bytes: 31 966 394". Подозреваю, что анализом одного лишь heap не обойтись.

Что интересно под виндой в диспетчере задач java-процесса занимает мало памяти - порядка 64Мб, как и положено. Может всё-таки дело не в утечках, а в какой-то области памяти типа кеша, про которую я ещё не знаю.


49.4k 72 72 золотых знака 250 250 серебряных знаков 480 480 бронзовых знаков


Вопрос сколько памяти на самом деле занимет программа вообще крайне не однозначен. Достаточно сказать, что в эти "RES" попадут страницы всех системных .so, к которым обращалась программа. Поскольку это общий ресурс (делится на всех в системе), то можно было бы и не учитывать его, только вот "отделить мух от котлет" задешево в *nix не получается. Спасибо за комментарий. Но если это системные библиотеки, значит есть способ посмотреть, какие из них вызываются процессом, и определить виновника такого большого потребления памяти. Вы не знаете случайно такой способ? Почитайте в man proc разделы о /proc/[pid]/maps, /proc/[pid]/statm, /proc/[pid]/status и анализируйте поведение своего процесса / (а вообще, чего еще можно ожидать от Java :)?) / Сразу хочу сказать, что в реальности такой анализ -- дело неблагодарное, поскольку (если в системе в целом нет проблем с памятью) Вас может волновать лишь активный набор страниц да cache miss rate (а вот как стандартными средствами вытащить статистику по ним, я не знаю)

Вы не можете контролировать то, что хотите контролировать. -Xmx влияет только на Java Heap и не влияет на потребление памяти JVM нативными средствами, которое зависит от реализации JVM.

Обслуживание динамической памяти Java и сборщика мусора требует использования нативной памяти, на которое вы не можете повлиять.

More native memory is required to maintain the state of the memory-management system maintaining the Java heap. Data structures must be allocated to track free storage and record progress when collecting garbage. The exact size and nature of these data structures varies with implementation, but many are proportional to the size of the heap.

Дополнительная нативная память требуется для поддерживания работы системы управления памятью Java. Необходимо где-то хранить структуры данных, отслеживающие доступную память и следящие за состоянием уборки мусора. Точный размер и природа этих структур данных зависит от конкретной реализации, но зачастую пропорционально зависит от размера выделяемой динамической памяти.

JIT-компилятор использует нативную память, как это делал бы компилятор Java.

Bytecode compilation uses native memory (in the same way that a static compiler such as gcc requires memory to run), but both the input (the bytecode) and the output (the executable code) from the JIT must also be stored in native memory. Java applications that contain many JIT-compiled methods use more native memory than smaller applications.

При компиляции байт-кода используется нативная память (точно так же, как статический компилятор, например gсc, использует память в процессе работы), да и как входные (байт-код) так и выходные данные (машинный код) JIT-компилятора приходится хранить в нативной памяти. То есть Java-приложения, содержащие много компилируемых JIT-ом методов требуют больше нативной памяти.

Загрузчики классов используют нативную память.

Java applications are composed of classes that define object structure and method logic. They also use classes from the Java runtime class libraries (such as java.lang.String) and may use third-party libraries. These classes need to be stored in memory for as long as they are being used. How classes are stored varies by implementation.

Java-приложения состоят из классов, которые определяют структуру и поведение объектов. Классы могут предоставляться как JRE (например java.lang.String ) так и сторонними библиотеками. Эти классы нужно хранить в памяти все время, пока они используются. Способ хранения в классов в памяти, зависит от реализации JVM.

И это не еще затрагивая потоки. Главное, что нужно понять: параметр -Xmx не контролирует всю выделяемую для JVM память, он влияет только на доступную внутри JVM динамическую память (Java Heap), но далеко не все хранится в ней.

Читайте также: