После завершённых циклов GC базовый объём занятой Java-памяти растёт от цикла к циклу. Какой вывод нужно проверить первым?
Сначала нужно проверить рост живого набора объектов: всё больше объектов остаётся достижимым после сборки мусора. Это признак удержания объектов или закономерного роста кэша, очередей и других структур состояния, но ещё не доказательство утечки.
Важно сравнивать объём памяти после завершённых циклов GC при сопоставимой нагрузке. Если базовый уровень стабильно растёт, следует искать удерживающую ссылку и определить, является ли рост ожидаемым.
Сборщики мусора проектировались с учётом того, что многие объекты в Java живут недолго. Поколенческая организация heap позволяет чаще обрабатывать молодые объекты и реже перемещать долгоживущие, снижая среднюю стоимость сборок.
Из этого следует практическое различие между временным давлением на память и ростом живого набора. Временные объекты исчезают после GC, а удерживаемые объекты продолжают занимать память и повышают базовый уровень использования heap.
Рост heap между сборками сам по себе ничего не доказывает: приложение может просто активно создавать короткоживущие объекты. Проблема возникает, если после сопоставимых сборок минимальный объём занятой памяти последовательно увеличивается и в итоге приближается к лимиту heap.
Неверный вывод может привести к бесполезному увеличению максимального heap. Это обычно лишь отсрочит OutOfMemoryError, увеличит длительность сборок и не устранит удерживающую ссылку.
Сначала нужно сопоставить несколько циклов по временной шкале: размер heap до GC, после GC, скорость аллокаций, длительность пауз и состав нагрузки. Если после сборок нижняя граница стабилизируется, наблюдалось временное allocation pressure. Если она растёт, увеличивается live set.
Затем следует найти владельцев объектов. Для этого используют heap dump, пути от GC Roots, дерево доминаторов и сравнительный анализ двух снимков. Особое внимание уделяют статическим коллекциям, кэшам без политики вытеснения, очередям, незавершённым задачам, замыканиям и объектам, связанным с долгоживущими потоками.
Рост live set не всегда является утечкой. Он может быть ожидаемым при прогреве кэша, накоплении результатов пакетной обработки или увеличении числа активных сессий. Нужно проверить, ограничен ли этот рост бизнес-условиями и освобождаются ли объекты после уменьшения соответствующей нагрузки.
Измерение следует проводить с учётом конкретного сборщика. Не каждый цикл у каждого сборщика немедленно обрабатывает весь heap, а concurrent-сборки могут показывать промежуточное состояние. Поэтому один снимок или одна неполная сборка недостаточны; важна серия наблюдений после завершённых релевантных циклов.
Увеличение heap допустимо как временная мера, если рост памяти соответствует ожидаемому увеличению рабочей нагрузки. Для диагностики оно опасно тем, что меняет частоту GC и может скрыть проблему. Надёжный вывод требует одновременно анализировать график live set, heap dump и жизненный цикл объектов.
Сервис после каждого часового пика нагрузки возвращался не к прежнему уровню heap, а к уровню примерно на 200 МБ выше. Рассматривались три варианта: увеличить heap, принудительно очищать кэш или снять сравнительные heap dump.
Увеличение heap дало запас, но повысило время последующих сборок. Принудительная очистка кэша уменьшила память, однако создала резкий рост обращений к базе данных. Сравнение dump до и после пика показало, что очередь фоновых задач сохраняла завершённые результаты до обработки всех связанных уведомлений.
Выбрали исправление жизненного цикла очереди: результат удалялся после подтверждения обработки, а размер очереди получил ограничение. После этого базовый объём памяти стабилизировался, а кэш сохранил контролируемую политику вытеснения. Увеличенный heap оставили только как запас для штатных пиков.
Нет. Это может быть нормальным ростом кэша, числа активных запросов, очереди или другого состояния приложения. Утечка подозревается тогда, когда объём живых объектов растёт без соответствующего роста ожидаемой рабочей нагрузки либо не уменьшается после завершения операций.
Один dump показывает состояние графа объектов в конкретный момент, но не показывает динамику. Крупный объект может быть временно необходим, а его retained size — закономерным следствием текущей операции. Для подтверждения нужны как минимум временная серия измерений и анализ путей от GC Roots, желательно с сопоставлением dump до и после одинакового сценария.
Это уже не обязательно проблема живых Java-объектов. RSS может увеличиваться из-за direct memory, памяти потоков, JNI, метаданных классов, библиотек, фрагментации аллокатора или не возвращённых операционной системе страниц. Нужно разделить Java heap и нативную память с помощью подходящих средств диагностики, а не делать вывод об утечке heap только по RSS.