Как устранить утечку памяти Java? Я стою на коленях после прочтения!

JVM

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

Утечки памяти происходят, когда ссылки на объекты, которые больше не нужны, поддерживаются без необходимости. Эти утечки оченьУпс. Во-первых, поскольку программы потребляют все больше и больше ресурсов, они создают ненужную нагрузку на компьютер. Что еще хуже, обнаружение этих утечек может быть затруднено: статический анализ часто изо всех сил пытается точно идентифицировать эти избыточные ссылки, а существующие инструменты обнаружения утечек отслеживают и сообщают детализированную информацию об отдельных объектах, давая неинтерпретируемый и недостаточно точный результат.

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

На самом деле существует четыре класса проблем с памятью с похожими и перекрывающимися характеристиками, но с разными причинами и решениями:

  • Производительность: часто связано с чрезмерным созданием и удалением объектов, длительными задержками при сборке мусора, чрезмерной подкачкой страниц ОС и т. д.
  • Ограничения ресурсов: когда доступно мало памяти или память слишком фрагментирована для размещения больших объектов — это может быть нативным или, что чаще всего, связано с кучей Java.
  • Утечки кучи Java (утечки кучи Java): классические утечки памяти, когда объекты Java постоянно создаются без освобождения. Обычно это вызвано ссылкой на базовый объект.
  • Утечки собственной памяти: связаны с любым растущим использованием памяти за пределами кучи Java, например, выделенной кодом JNI, драйверами или даже JVM.

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

Но прежде чем вы сможете предотвращать и обнаруживать утечки памяти, вы должны понять, как и почему они происходят. (Примечание: если вы хорошо справляетесь со сложными утечками памяти, вы можетеперепрыгни. )

1. Утечки памяти: основы

Во-первых, думайте об утечках памяти как о болезни, а OutOfMemoryError Java (сокращенно OOM) — как о ее симптоме. Но, как и в случае любой болезни, не все OOM подразумевают утечку памяти: OOM могут возникать из-за генерации большого количества локальных переменных или других подобных событий. С другой стороны, не все утечки памяти обязательно проявляются как OOM, особенно в случае настольных приложений или клиентских приложений (которые работают в течение длительного времени без перезапуска).

Считайте утечки памяти болезнью, а OutOfMemoryError — симптомом. Но не все OutOfMemoryErrors означают утечки памяти, не все утечки памяти проявляются как OutOfMemoryErrors.

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

2. Расшифровать OutOfMemoryError

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

Первый шаг в диагностике OOM - определить, что значит ошибка. Это звучит ясно, но ответ не всегда ясно. Например: происходит ли ума, потому что куча Java заполнена, или родная куча заполнена? Чтобы помочь вам ответить на этот вопрос, давайте проанализируем некоторые возможные сообщения об ошибках:

  • java.lang.OutOfMemoryError: Java heap space
  • java.lang.OutOfMemoryError: PermGen space
  • java.lang.OutOfMemoryError: Requested array size exceeds VM limit
  • java.lang.OutOfMemoryError: request bytes for . Out of swap space?
  • java.lang.OutOfMemoryError: (Native method)

2.1. "Java куча пространства"

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

Например, я отвечаю за профилирование приложения, которое постоянно выдает этот тип ошибки OutOfMemoryError. После некоторого расследования я обнаружил, что виновником было создание экземпляра массива, потому что требовалось слишком много памяти; в этом случае это не ошибка приложения, а зависимость сервера приложений от слишком маленькой кучи по умолчанию. я приспосабливаюсьПараметры памяти JVMрешил эту проблему.

В других случаях, особенно для долгоживущих приложений, сообщение может указывать на то, что мы непреднамеренно удерживаем ссылки на объекты, препятствуя их очистке сборщиком мусора. На данный момент язык Java эквивалентен утечке памяти. (Примечание: API, вызываемые приложениями, также могут непреднамеренно содержать ссылки на объекты.)

Другим потенциальным источником этих OOM "пространства кучи Java" является использование финализаторов. Если у класса есть метод finalize, объекты этого типа не будут утилизированы при сборке мусора. Вернее, после сборки мусора позже объект будет поставлен в очередь на доработку. В реализации Sun финализаторы определяютсяНить демонавоплощать в жизнь. Если потоки финализаторов не успевают за очередью финализации, куча Java может заполниться, и OOM может быть выброшен.

2.2.«ПермГен Пространство»

Это сообщение об ошибке указывает, чтопостоянное поколениеполный. Постоянная генерация — это область кучи, в которой хранятся объекты классов и методов. Если ваше приложение загружает большое количество классов, вам может потребоваться увеличить размер постоянного поколения с помощью параметра -XX:MaxPermSize.

InternedОбъекты java.lang.String также хранятся в постоянном поколении. Класс java.lang.String поддерживает пул строк. Когда вызывается внутренний метод, он проверяет пул, чтобы увидеть, существует ли эквивалентная строка. Если да, то он возвращается внутренним методом, если нет, то строка добавляется в пул. Точнее, метод java.lang.String.intern возвращает каноническое представление строки; результатом является экземпляр того же класса, который будет возвращен, когда эта строка будет отображаться как литерал.Цитировать. Если ваше приложение создает большое количество строк, вам может потребоваться увеличить размер постоянного поколения.

Примечание. Вы можете использовать команду jmap -permgen для вывода статистики, связанной с постоянной генерацией, включая информацию о внутренних экземплярах String.

2.3. «Запрошенный размер массива превышает лимит ВМ»

Эта ошибка указывает на то, что приложение (или API, используемый этим приложением) попыталось выделить массив, размер которого превышает размер кучи. Например, если приложение пытается выделить массив размером 512 МБ, но максимальный размер кучи составляет 256 МБ, для этого сообщения об ошибке будет выдано сообщение об ошибке OOM. В большинстве случаев проблема связана с конфигурацией или ошибкой, вызванной попыткой приложения выделить большой массив.

2.4. "Запросить байты для . Недостаточно места подкачки?"

Это сообщение похоже на OOM. Однако HotSpot VM выдает это исключение, когда происходит сбой выделения собственной кучи, и эта куча может быть исчерпана. Сообщение включает размер неудачного запроса (в байтах) и причину запроса памяти. В большинстве случаев это имя исходного модуля, который сообщил об ошибке выделения.

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

  • Недостаточно места подкачки, настроенного операционной системой.
  • Другой процесс в системе потребляет все доступные ресурсы памяти.

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

2.5. (Native method)

Если вы видите это сообщение об ошибке, а верхний кадр трассировки стека является собственным методом, собственный метод обнаружил ошибку выделения. Разница между этим сообщением и предыдущим заключается в том, что сбой выделения памяти Java был обнаружен в JNI или собственном методе, а не в коде Java VM.

Если выдается этот тип OOM, вам может потребоваться использовать утилиту в операционной системе для дальнейшей диагностики проблемы.

2.6.Application Crash Without OOM

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

Например, системный вызов malloc вернет NULL, если нет доступной памяти. Если возврат malloc не проверяется, приложение может аварийно завершить работу при попытке доступа к недопустимой ячейке памяти. В зависимости от ситуации обнаружить такие проблемы может быть сложно.

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

3. Диагностика утечек

В большинстве случаев диагностика утечек памяти требует очень подробного понимания рассматриваемого приложения. Предупреждение: процесс может быть долгим и повторяющимся.

Наша стратегия поиска утечек памяти будет относительно простой:

  1. Определите симптомы
  2. Включить подробную сборку мусора
  3. Включить аналитику
  4. Трассировка анализа

3.1 Определите симптомы

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

Часто, если приложение Java запрашивает больше памяти, чем предоставляет куча времени выполнения, это может быть связано с плохим дизайном. Например, если приложение создает несколько копий изображения или загружает файл в массив, ему не хватит места для хранения, когда изображение или файл очень велики. Это нормальное исчерпание ресурсов. Приложение работает как задумано (хотя этот дизайн явно глупый).

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

3.2 Включить подробную сборку мусора

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

В частности, параметр -verbosegc позволяет создавать трассировки в начале каждого процесса сборки мусора (GC). То есть при сборке мусора в памяти печатается сводный отчет со стандартной ошибкой, дающий представление о том, как осуществляется управление памятью.

Вот некоторые типичные выходные данные, сгенерированные с опцией -verbosegc:

image

Каждый блок (или раздел) в этом файле трассировки сборщика мусора пронумерован в порядке возрастания. Чтобы понять эту трассировку, вы должны посмотреть на последовательные секции сбоя выделения и посмотреть, освобождается ли память (в байтах и ​​процентах), уменьшающаяся со временем, в то время как общая память (здесь, 19725304) увеличивается. Это типичные признаки истощения памяти.

3.3 Включить аналитику

Различные JVM предоставляют разные методы создания файлов трассировки для отражения активности кучи, и эти методы часто включают сведения о типе и размере объектов. Это называется кучей анализа.

3.4 Путь анализа

В этой статье основное внимание уделяется трассировкам, созданным Java VisualVM. Трассировки могут иметь разные форматы, так как они могут генерироваться разными инструментами обнаружения утечек памяти Java, но их идея всегда одна и та же: найти фрагменты объектов в куче, которых там быть не должно, и определить, накапливаются ли эти объекты. вместо освобождения. Особый интерес представляют временные объекты, которые известны каждый раз, когда в приложении Java запускается определенное событие. Должно быть всего несколько, но много экземпляров объекта, обычно это указывает на ошибку приложения.

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

4. Как работает сборка мусора в JVM?

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

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

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

Основываясь на этом предположении, [Java делит объекты на несколько поколений](Woohoo. Сеть Oracle.com/tech…, Поколения|схема). Вот наглядное объяснение:

image

  • Молодое поколение - это начало объекта. у него два потомка
    • Пространство Эдема — здесь начинаются объекты. Большинство объектов создаются и уничтожаются в Eden Space. Здесь сборщик мусора выполняет второстепенные сборщики мусора, которые представляют собой оптимизированную сборку мусора. Когда выполняется дополнительный сборщик мусора, любые ссылки на объекты, которые все еще необходимы, переносятся в одно из оставшихся пространств (S0 или S1).
    • Survivor Space (S0 и S1) — здесь оказались объекты, пережившие Eden Space. Их два, и только один используется в любой момент времени (если только у нас нет серьезной утечки памяти). Один обозначается как пустой, а другой как активный, чередующийся с каждым циклом GC.
  • Постоянное поколение — также известное как старое поколение (старое пространство на рисунке 2), это пространство содержит объекты, которые живут дольше и имеют более длительный срок службы (если они живут достаточно долго, они перемещаются из пространства выживших). При заполнении этого места сборщик мусора выполняет полный сборщик мусора, что снижает затраты с точки зрения производительности. Если это пространство увеличивается до бесконечности, JVM выдает OutOfMemoryError — пространство кучи Java.
  • Постоянное поколение. Поскольку третье поколение тесно связано с постоянным поколением, постоянное поколение является особенным, поскольку оно содержит данные, необходимые виртуальной машине для описания объектов, которые не имеют эквивалента на уровне языка Java. Например, объекты, описывающие классы и методы, хранятся в постоянной генерации.

Java достаточно умен, чтобы применять разные методы сборки мусора для каждого поколения. Молодое поколение обрабатывается с помощью сборщика отслеживающих копий под названием Parallel New Collector. Этот коллектор блокирует мир, но поскольку молодое поколение обычно малочисленно, пауза недолгая.

Дополнительные сведения о генерации JVM и о том, как это работает, см.Управление памятью в виртуальной машине Java HotSpot™.

5 Обнаружение утечек памяти

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

5.1 Использование Java VisualVM для удаленного анализа кучи

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

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

Чтобы воспользоваться всеми функциями Java VisualVM, вы должны использовать Java Platform Standard Edition (Java SE) версии 6 или более поздней.

Related: Why You Need to Upgrade to Java 8 Already

5.2 Включение удаленных подключений для JVM

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

Во-первых, нам нужно предоставить себе доступ к JVM на целевой машине. Для этого создайте файл с именем jstatd.all.policy следующего содержания:

grant codebase "file:${java.home}/../lib/tools.jar" {

   permission java.security.AllPermission;

};

После создания файла нам нужно использоватьjstatd - Virtual Machine jstat DaemonИнструмент позволяет удаленно подключаться к целевой виртуальной машине следующим образом:

jstatd -p <PORT_NUMBER> -J-Djava.security.policy=<PATH_TO_POLICY_FILE>

Например:

jstatd -p 1234 -J-Djava.security.policy=D:\jstatd.all.policy

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

5.3. Подключение к удаленному хосту

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

Далее нам нужно добавить удаленный хост в VisualVM. Когда целевая JVM включена для разрешения удаленных подключений с другого компьютера с J2SE 6 или выше, мы запускаем инструмент Java VisualVM и подключаемся к удаленному хосту. Если подключение к удаленному хосту прошло успешно, мы увидим, как приложение Java работает на целевой JVM следующим образом:

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

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

6. MemLeak

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

HashMap — это интерфейс карты.хеш-таблицареализации, поэтому он определяет базовую концепцию ключей и значений: каждое значение связано с уникальным ключом, поэтому, если ключ данной пары ключ-значение уже существует в HashMap, его текущее значение заменяется.

Наш ключевой класс должен обеспечить правильную реализацию методов equals() и hashcode(). Без них нет гарантии, что будет сгенерирован хороший ключ.

По не определяем методы equals() и hashcode(), мы добавляем один и тот же ключ снова и снова, не заменяя ключ, Hashmap растет, и не может идентифицировать эти одинаковые ключи и выкидывает OutofMemoryError.

Класс MemLeak:

package com.post.memory.leak;

import java.util.Map;

public class MemLeak {

    public final String key;

    public MemLeak(String key) {
        this.key =key;
    }

    public static void main(String args[]) {
        try {
            Map map = System.getProperties();
            for(;;) {
                map.put(new MemLeak("key"), "value");
            }
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
}

Примечание: утечка памяти не связана с бесконечным циклом в строке 14: бесконечный цикл может привести к исчерпанию ресурсов, но не приведет к утечке памяти. Если мы правильно реализовали методы equals() и hashcode(), код будет работать нормально даже с бесконечным циклом, поскольку у нас есть только один элемент в HashMap.

(Для интересующихся,здесьЕсть несколько альтернативных способов (преднамеренно) генерировать утечки. )

7. Использование Java VisualVM

Используя Java VisualVM, мы можем выполнять мониторинг памяти в куче Java и определять ее поведение в отношении утечек памяти.

Это графическое представление профилировщика кучи Java MemLeak сразу после инициализации (вспомните нашПоколенияобсуждение):

image

Всего через 30 секунд старое поколение почти заполнено, что указывает на то, что старое поколение растет даже с полным сборщиком мусора, что является явным признаком утечки памяти.

Ниже показан один из способов обнаружения причины этой утечки (щелкните, чтобы увеличить), сгенерированный с помощью Java VisualVM с heapdump. Здесь мы видим, что 50% объектов Hashtable$Entry находятся в куче, а вторая строка указывает на класс MemLeak. Таким образом, утечка памяти вызвана хэш-таблицей, используемой в классе MemLeak.

image

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

image

8. Заключение

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

9. Приложение

Наряду с Java VisualVM существует несколько других инструментов, которые могут выполнять обнаружение утечек памяти. Многие детекторы утечек работают на уровне библиотек, перехватывая вызовы процедур управления памятью. Например, HPROF — это простой инструмент командной строки в комплекте с платформой Java 2 Standard Edition (J2SE) для анализа кучи и ЦП. Выходные данные HPROF можно анализировать напрямую или использовать в качестве входных данных для других инструментов, таких как JHAT. Когда мы используем приложения Java 2 Enterprise Edition (J2EE), существует множество более удобных решений для анализа дампа кучи, напримерIBM Heapdumps для сервера приложений Websphere.

оригинал:Woohoo.topthem.com/Java/Тело души, которое…

Хосе Феррейрад Соуза Филью

Переводчик: Эмма