Делаем утечки памяти в Java быстро и без регистрации
Аннотация
Все знают, что в Java есть сборщик мусора, и она сама очищает неиспользуемую память. Но этот механизм может работать не всегда идеально: можно написать код таким образом, что Java не сможет очистить память и она будет копиться пока приложение не упадет по OutOfMemoryError. На простых примерах рассмотрим несколько случаев, когда это может произойти. А в заключении поговорим как искать утечки памяти с использованием встроенных инструментов JDK.
План
- Как устроена память в Java.
- Как в Java осуществляется поиск неиспользуемых объектов?
- Рассмотрим несколько примеров, когда возникает утечка памяти:
- Неверная реализация
equals
иhashCode
. - Статические члены класса.
- Пытаемся исправить утечку памяти с помощью WeakReference.
- ThreadLocal переменные при использовании пула потоков.
- Нестатические внутренние классы.
- Пул строк (String interning).
- Неверная реализация
- Как найти утечку памяти (работаем с
jmap
,jstack
иjhsdb
); - Отладка приложения через консоль.
- Выводы: простые правила как не допускать утечек памяти.
Доклад
Как устроена память в Java
Память делится на Heap, Metaspace и Stack.
- Heap – основной сегмент памяти, используется для выделения памяти под объекты и JRE классы. Создание нового объекта происходит в Heap, здесь работает GC.
- Metaspace – хранятся метаданные о классе и статические поля: там хранятся это либо примитивы, либо ссылки на
объекты/массивы, которые сами по себе аллоцированы в Heap. Metaspace в Java 8 пришел на замену PermGen,
основное отличие которой — возможность динамически расширятся, ограниченная по умолчанию только размером нативной
памяти. Опционально можно задать размер через аргумент
-XX:MaxMetaspaceSize
. В боевых окружениях желательно всегда задавать размер Metaspace. В случае возникновения ошибки, лечится увеличением Metaspace, либо добавлением памяти. - Stack – стековая память в Java работает по схеме LIFO: всякий раз, когда вызывается метод, в памяти стека создается новый блок, который содержит примитивы и ссылки на другие объекты в методе. Каждый поток имеет свой стек, примитивы и ссылки на локальные переменные хранятся в стеке. Как только метод заканчивает работу, блок также перестает использоваться, тем самым предоставляя доступ для следующего метода. Объекты в куче доступны с любой точки программы, в то время как стековая память не может быть доступна для других потоков.
On-heap memory is memory in the Java heap, which is a region of memory managed by the garbage collector. Java objects reside in the heap. The heap can grow or shrink while the application runs. When the heap becomes full, garbage collection is performed: The JVM identifies the objects that are no longer being used (unreachable objects) and recycles their memory, making space for new allocations. Off-heap memory is memory outside the Java heap. To invoke a function or method from a different language such as C from a Java application, its arguments must be in off-heap memory. Unlike heap memory, off-heap memory is not subject to garbage collection when no longer needed. You can control how and when off-heap memory is deallocated.
Как в Java осуществляется поиск неиспользуемых объектов?
В Java процесс работы с памятью скрыт от программиста: JVM сама занимается выделением памяти и ее очисткой. Процесс очистки памяти называется Garbage Collection. Из названия следует, что GC занимается очисткой памяти, т.е. удаляет неиспользуемые объекты из памяти. Весь процесс состоит из двух частей:
- mark – обход дерева объектов и поиск достижимых ссылок из корневых объектов (GC Roots).
- sweep – удаление неиспользуемых объектов.
Поиск мусора:
- Reference counting – у каждого объекта счетчик ссылок. Когда он равен нулю, объект считается мусором. В случае обнаружения цикличных ссылок, объекты считаются недостижимыми, если на них не ссылаются никакие другие объекты.
- Tracing - объект считается не мусором, если до него можно добраться с корневых точек (GC Roots).
Корневые точки (GC Roots):
- Классы, загруженные системным ClassLoader’ом. Эти классы никогда не могут быть выгружены.
- Активные потоки.
- Локальные переменные, параметры методов.
- Объекты, используемые в мониторе для синхронизации.
- JNI (Java Native Interface).
- Объекты, огражденные от сборки мусора самим JVM.
Примеры
- OutOfMemoryError: Java heap space. <– рассмотрим этот класс ошибок.
- OutOfMemoryError: Metaspace.
- OutOfMemoryError: Requested array size exceeds VM limit.
- OutOfMemoryError: Unable to create new native thread.
- OutOfMemoryError: GC Overhead limit exceeded.
Неверная реализация equals
и hashCode
Пример: EqualsAndHashCodeExample.
Запуск: ./gradlew runEqualsAndHashCodeExample
.
Статические переменные
Пример: StaticResourcesExample.
Запуск: ./gradlew runStaticResourcesExample
.
Используем WeakReference
Пример: StaticResourcesWeakReferenceExample.
Запуск: ./gradlew runStaticResourcesWeakReferenceExample
.
Значение очищается, но ключ остается. Нужно использовать java.util.WeakHashMap
.
ThreadLocal переменные при использовании пула потоков
Пример: ThreadLocalExample.
Запуск: ./gradlew runThreadLocalExample
.
By definition, a reference to a ThreadLocal value is kept until the “owning” thread dies or if the ThreadLocal itself is no longer reachable.
Пул строк (String interning)
Пример: InternalStringsExample.
Запуск: ./gradlew runInternalStringsExample
.
Prior to Java 7 interned strings were allocated in PermGen space. This would become a garbage collector issue once your string is of no more use in application, since the interned string pool is a static member of the String class and will never be garbage collected. From Java 7 onward the interned strings are allocated on the Heap and are subject to garbage collection.
TODO
- Внутренние классы.
- Как исправить статические члены класса с WeakReference. Использование WeakReference вместо WeakHashMap.
Как найти утечку памяти?
Какого-то универсального алгоритма поиска утечки памяти нет, но вот список основных действий, которые нужно выполнить:
- Включить параметр JWM
-XX:+HeapDumpOnOutOfMemoryError
для получения heap dump при падении по OutOfMemoryError. После этого загрузить результат в JProfiler или подобный инструмент и посмотреть какие ваши объекты занимают много памяти (хотя не должны). - Провести статический анализ кода на предмет утечек памяти (современные анализаторы умеют искать причины OOM).
- Возможно, просто нужно увеличить ресурсы приложения: возросла нагрузка или усложнились бизнес-операции.
- Если вы используете JNI (Java Native Interface) или другие средства нативного взаимодействия с памятью, то постарайтесь уйти от этого. Возможно, лучше написать сервер на C++ и коммуницировать с ним через socket, чем напрямую работать с памятью из Java (она для этого не приспособлена).
$ jps -l
$ jstack <PID> > threaddump.txt
$ jhsdb jmap --heap --pid 62807
$ jmap -histo <PID> | head
$ jmap -dump:format=b,file=heapdump.hprof <PID>
Удаленная отладка
Для подключения debugger’а нужно запустить с флагом:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=127.0.0.1:8008
.
$ jdb -attach 8000
$ stop at ru.romanow.memory.leaks.StaticResourcesExample:19
$ print ru.romanow.memory.leaks.StaticResourcesExample.cache.size()
$ locals
$ step into
$ step up
$ resume
$ clear ru.romanow.memory.leaks.StaticResourcesExample:19
Выводы: список простых правил как бороться с утечками памяти
Keep it simple.