Утечки памяти
Материал на этой странице устарел, поэтому скрыт из оглавления сайта.
Утечки памяти происходят, когда браузер по какой-то причине не может освободить память от недостижимых объектов.
Обычно это происходит автоматически (Управление памятью в JavaScript). Кроме того, браузер освобождает память при переходе на другую страницу. Поэтому утечки в реальной жизни проявляют себя в двух ситуациях:
- Приложение, в котором посетитель все время на одной странице и работает со сложным JavaScript-интерфейсом. В этом случае утечки могут постепенно съедать доступную память.
- Страница регулярно делает что-то, вызывающее утечку памяти. Посетитель (например, менеджер) оставляет компьютер на ночь включённым, чтобы не закрывать браузер с кучей вкладок. Приходит утром – а браузер съел всю память
и рухнули сильно тормозит.
Утечки бывают из-за ошибок браузера, ошибок в расширениях браузера и, гораздо реже, по причине ошибок в архитектуре JavaScript-кода. Мы разберём несколько наиболее частых и важных примеров.
Коллекция утечек в IE
Утечка DOM ↔ JS в IE8-
IE до версии 8 не умел очищать циклические ссылки, появляющиеся между DOM-объектами и объектами JavaScript. В результате и DOM и JS оставались в памяти навсегда.
В браузере IE8 была проведена серьёзная работа над ошибками, но утечка в IE8- появляется, если круговая ссылка возникает «через объект».
Чтобы было понятнее, о чём речь, посмотрите на следующий код. Он вызывает утечку памяти в IE8-:
function leak() < // Создаём новый DIV, добавляем к BODY var elem = document.createElement('div'); document.body.appendChild(elem); // Записываем в свойство жирный объект elem.__expando = < bigAss: new Array(1000000).join('lalala') >; // Создаём круговую ссылку. Без этой строки утечки не будет. elem.__expando.__elem = elem; // Удалить элемент из DOM. Браузер должен очистить память. elem.parentElement.removeChild(elem); >
Полный пример (только для IE8-, а также IE9 в режиме совместимости с IE8):
Понимание утечек памяти в Java
Одним из основных преимуществ Java является автоматизированное управление памятью с помощью встроенного сборщика мусора (или сокращенно GC). GC неявно заботится о выделении и освобождении памяти и, таким образом, способен решать большинство проблем, связанных с ее утечкой.
Хотя GC эффективно обрабатывает значительную часть памяти, он не гарантирует надежного решения проблемы с ее утечкой. GC достаточно умен, но не безупречен. Утечки памяти все еще могут закрасться даже в приложения, созданные добросовестным разработчиком.
По-прежнему возможны ситуации, когда приложение создает значительное количество лишних объектов, расходуя ресурсы памяти, что иногда приводит к его полному отказу.
Утечки памяти — это настоящая проблема в Java. В этом руководстве мы рассмотрим, каковы потенциальные причины утечек, как распознавать их в рантайме и как справиться с ними в нашем приложении.
2. Что такое утечка памяти
Утечка памяти — это ситуация, когда в куче присутствуют объекты, которые больше не используются, но сборщик мусора не может удалить их из памяти и, таким образом, они сохраняются там без необходимости.
Утечка памяти плоха тем, что она блокирует ресурсы памяти и со временем снижает производительность системы. Если с ней не бороться, приложение в конечном итоге исчерпает свои ресурсы и завершится с фатальной ошибкой java.lang.OutOfMemoryError .
Существует два различных типа объектов, которые находятся в Heap-памяти (куче) — со ссылками и без них. Объекты со ссылками — это те, на которые имеются активные ссылки внутри приложения, в то время как на другие нет таких ссылок.
Сборщик мусора периодически удаляет объекты без ссылок, но он никогда не собирает объекты, на которые все еще ссылаются. В таких случаях могут возникать утечки памяти:
Признаки утечки памяти
- Серьезное снижение производительности при длительной непрерывной работе приложения
- Ошибка кучи OutOfMemoryError в приложении
- Спонтанные и странные сбои приложения
- В приложении время от времени заканчиваются объекты подключения
Давайте подробнее рассмотрим несколько таких сценариев и как с ними бороться.
3. Типы утечек памяти в Java
В любом приложении утечка памяти может произойти по множеству причин. В этом разделе мы обсудим наиболее распространенные из них.
3.1. Утечка памяти через статические поля
Первый сценарий, который может привести к потенциальной утечке памяти, — это интенсивное использование статических переменных.
В Java статические поля имеют срок жизни, который обычно соответствует полному жизненному циклу запущенного приложения (за исключением случаев, когда ClassLoader получает право на сборку мусора).
Давайте создадим простую Java-программу, которая заполняет статический список:
public class StaticTest < public static Listlist = new ArrayList<>(); public void populateList() < for (int i = 0; i < 10000000; i++) < list.add(Math.random()); >Log.info("Debug Point 2"); > public static void main(String[] args) < Log.info("Debug Point 1"); new StaticTest().populateList(); Log.info("Debug Point 3"); >>
Теперь, если мы проанализируем кучу во время выполнения этой программы, то увидим, что она увеличилась между точками отладки 1 и 2.
Но когда мы оставляем метод populateList() в точке отладки 3, куча еще не убрана сборщиком, как это видно в ответе VisualVM:
Однако в приведенной выше программе, в строке номер 2, если мы просто отбросим ключевое слово static, то это приведет к резкому изменению использования памяти, как показывает отклик:
Первая часть до точки отладки почти не отличается от того, что мы получили в случае static. Но на этот раз после выхода из метода populateList() вся память списка очищается, поскольку у нас нет на него ссылок.
Следовательно, нам нужно очень внимательно следить за использованием статических переменных. Если коллекции или большие объекты объявлены как статические, то они остаются в памяти на протяжении всего времени работы приложения, тем самым блокируя жизненно важную память, которую можно было бы использовать в другом месте.
Как предотвратить это?
- Минимизируйте использование статических переменных
- При использовании синглтонов полагайтесь на имплементацию, которая лениво, а не жадно загружает объект.
3.2. Через незакрытые ресурсы
Всякий раз, когда мы создаем новое соединение или открываем поток, JVM выделяет память для этих ресурсов. В качестве примера можно привести соединения с базой данных, входные потоки и объекты сессий.
Забыв закрыть эти ресурсы, можно заблокировать память, что сделает их недоступными для GC. Это может произойти даже в случае исключения, которое не позволяет программному процессу достичь оператора, выполняющего код для закрытия этих ресурсов.
В любом случае, открытые соединения, оставшиеся от ресурсов, потребляют память, и если с ними не разобраться, они могут ухудшить производительность и даже привести к ошибке OutOfMemoryError .
Как предотвратить это?
- Всегда используйте блок finally для закрытия ресурсов
- Код (даже в блоке finally ), закрывающий ресурсы, сам не должен содержать исключений.
- При использовании Java 7+ можно использовать блок try-with-resources .
3.3. Неправильная имплементация equals() и hashCode()
При определении новых классов очень распространенной ошибкой является отсутствие надлежащих переопределенных методов для equals() и hashCode() .
HashSet и HashMap используют эти методы во многих операциях, и если они переопределены неправильно, то могут стать источником потенциальных проблем с утечкой памяти.
Давайте рассмотрим как пример тривиальный класс Person и используем его в качестве ключа в HashMap
public class Person < public String name; public Person(String name) < this.name = name; >>
Теперь мы вставим дубликаты объектов Person в Map , использующую этот ключ.
Помните, что Map не может содержать дубликаты ключей:
@Test public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() < Mapmap = new HashMap<>(); for(int i=0; i Assert.assertFalse(map.size() == 1); >
Здесь мы используем Person в качестве ключа. В связи с тем, что Map не допускает дублирования ключей, то мы вставили в качестве ключа дубликаты объектов Person , что не должно увеличивать память.
Но поскольку мы не определили правильный метод equals() , дубликаты объектов накапливаются и увеличивают память, поэтому в памяти мы видим больше одного объекта. Куча в VisualVM в этом случае выглядит следующим образом:
Однако, если бы мы правильно переопределили методы equals() и hashCode(), то в этой Map существовал бы только один объект Person.
Давайте рассмотрим правильную имплементацию equals() и hashCode() для нашего класса Person :
public class Person < public String name; public Person(String name) < this.name = name; >@Override public boolean equals(Object o) < if (o == this) return true; if (!(o instanceof Person)) < return false; >Person person = (Person) o; return person.name.equals(name); > @Override public int hashCode() < int result = 17; result = 31 * result + name.hashCode(); return result; >>
В этом случае будут верны следующие утверждения:
@Test public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() < Mapmap = new HashMap<>(); for(int i=0; i Assert.assertTrue(map.size() == 1); >
После правильного переопределения equals() и hashCode() куча для той же программы выглядит следующим образом:
Другой пример — использование инструмента ORM, такого как Hibernate, который применяет методы equals() и hashCode() для анализа объектов и сохраняет их в кэше.
Вероятность утечки памяти довольно высока, если эти методы не переопределены, поскольку Hibernate не сможет сравнивать объекты и заполнит свой кэш их копиями.
Как предотвратить это?
- Как правило, на практике, при определении новых сущностей всегда переопределяйте методы equals() и hashCode() .
- Недостаточно их просто переопределить, это необходимо сделать оптимальным образом. Для получения дополнительной информации ознакомьтесь с нашими учебными пособиями Generate equals() and hashCode() with Eclipse и Guide to hashCode() in Java.
3.4. Внутренние классы, которые ссылаются на внешние
Это происходит в случае нестатических внутренних классов (анонимных классов). Для инициализации они всегда требуют экземпляр внешнего класса.
Каждый нестатический внутренний класс по умолчанию имеет неявную ссылку на содержащий его класс. Если мы используем объект этого внутреннего класса в приложении, то даже после того, как объект нашего содержащего внешнего класса покинет область видимости, он не будет убран в мусор.
Рассмотрим класс, который содержит ссылки на множество громоздких объектов и имеет нестатический внутренний класс. При создании объекта только внутреннего класса, модель памяти выглядит следующим образом:
Однако, если мы просто объявим внутренний класс как статический, то память уже будет выглядеть так:
Как предотвратить это?
- Если внутреннему классу не нужен доступ к членам внешнего класса, подумайте о том, чтобы превратить его в статический.
3.5. Через методы finalize()
Использование финализаторов — еще один источник потенциальных проблем с утечкой памяти. Когда метод finalize() класса переопределяется, то объекты этого класса не сразу убирают в мусор. Вместо этого GC ставит их в очередь на финализацию, которая происходит позже.
Кроме того, если код метода finalize() , не является оптимальным, а также очередь финализации не успевает за сборщиком мусора Java, то рано или поздно приложение столкнется с ошибкой OutOfMemoryError .
Для демонстрации возьмем класс, в котором мы переопределили метод finalize() , и его выполнение занимает немного времени. Когда большое количество объектов данного класса собирается в мусор, то в VisualVM это выглядит так:
Однако если мы просто удалим переопределенный метод finalize() , то та же программа даст следующий ответ:
Как предотвратить это?
- Мы всегда должны избегать финализаторов
Более подробно о finalize() читайте в разделе 3 (Как избежать использования финализаторов) нашего руководства по методу finalize в Java.
3.6. Интернированные строки
Пул строк Java претерпел значительные изменения в Java 7, когда он был перенесен из PermGen в HeapSpace. Однако для приложений, работающих на версии 6 и ниже, мы должны быть более внимательны при работе с большими строками.
Если мы считываем огромный объект-массив String и вызываем для него intern(), то он попадает в пул строк, который находится в PermGen (постоянной памяти) и будет оставаться там до тех пор, пока работает наше приложение. Это блокирует память и создает большую ее утечку в нашем приложении.
PermGen для этого случая в JVM 1.6 выглядит в VisualVM следующим образом :
В отличие от этого, если мы просто читаем строку из файла и не интернируем ее, PermGen выглядит так:
Как предотвратить это?
- Самый простой способ решить эту проблему — обновить Java до последней версии, так как начиная с Java версии 7 пул строк перемещен в HeapSpace.
- При работе с большими строками увеличьте размер пространства PermGen, чтобы избежать возможных ошибок OutOfMemoryErrors :
-XX:MaxPermSize=512m
3.7. Использование ThreadLocals
ThreadLocal (подробно рассматривается в учебнике «Введение в ThreadLocal в Java«) — это конструкция, которая дает нам возможность изолировать состояние для конкретного потока, тем самым позволяя достичь его безопасности.
В этой конструкции каждый поток хранит неявную ссылку на копию переменной ThreadLocal и будет поддерживать только свою собственную независимую копию, вместо совместного использования ресурса несколькими потоками, в течение всего времени, пока поток активен.
Несмотря на все преимущества, переменные ThreadLocal являются спорными, поскольку они могут приводить к утечкам памяти при неправильном использовании. Joshua Bloch однажды прокомментировал применение локальных переменных потоков:
Неаккуратное использование пулов потоков в сочетании с небрежным применением локальных переменных потоков может привести к непреднамеренному удержанию объектов, как было отмечено во многих местах. Но возлагать вину на локальные переменные неоправданно.
Утечки памяти при использовании ThreadLocals
Предполагается, что ThreadLocals будут утилизироваться, как только удерживающий их поток перестанет существовать. Но проблема возникает, когда ThreadLocals применяются вместе с современными серверами приложений.
Современные серверы приложений используют пул потоков для обработки запросов вместо создания новых (например, Executor в Apache Tomcat). Более того, они также используют отдельный загрузчик классов.
Поскольку пулы потоков в серверах приложений работают на основе концепции повторного использования, они никогда не утилизируются — их используют повторно для обслуживания другого запроса.
Теперь, если какой-либо класс создает переменную ThreadLocal , но явно не удаляет ее, то копия этого объекта останется в воркере Thread даже после остановки веб-приложения, тем самым препятствуя утилизации объекта.
Как предотвратить это?
- Хорошей практикой является очистка ThreadLocals , когда они больше не используются — ThreadLocals предоставляет метод remove(), который удаляет значение текущего потока для этой переменной.
- Не используйте ThreadLocal.set(null) для очистки значения — он в действительности не очищает, а вместо этого ищет Map , связанную с текущим потоком, и устанавливает пару ключ-значение как текущий поток и null соответственно
- Еще лучше рассматривать ThreadLocal как ресурс, который должен быть закрыт в блоке finally , чтобы быть уверенным в его закрытии во всех случаях, даже при исключении:
try < threadLocal.set(System.nanoTime()); //. further processing >finally
4. Другие стратегии борьбы с утечками памяти
Хотя универсального решения при борьбе с утечками памяти не существует, есть некоторые способы, с помощью которых их можно минимизировать.
4.1. Включить профилирование
Профилировщики Java — это инструменты, которые отслеживают и диагностируют утечки памяти в приложении. Они анализируют, что происходит внутри нашего приложения — например, как выделяется память.
Используя профилировщики, можно сравнить различные подходы и найти области, где оптимально используются наши ресурсы.
В разделе 3 этого руководства мы использовали Java VisualVM. Пожалуйста, ознакомьтесь с нашим руководством по профилировщикам Java, чтобы узнать о различных типах профилировщиков, таких как Mission Control, JProfiler, YourKit, Java VisualVM и Netbeans Profiler.
4.2. Подробная сборка мусора
При активации подробной сборки мусора мы отслеживаем детальную трассировку GC. Чтобы включить эту функцию, нам нужно добавить следующее в конфигурацию JVM:
-verbose:gc
Добавив этот параметр, мы сможем увидеть подробности того, что происходит внутри GC:
4.3. Использование ссылочных объектов для предотвращения утечек памяти
Для борьбы с утечками памяти можно также воспользоваться ссылочными объектами в Java, которые поставляются с пакетом java.lang.ref. С помощью пакета java.lang.ref вместо прямых ссылок на объекты мы используем специальные ссылки, которые позволяют легко собирать мусор.
Очереди ссылок предназначены для того, чтобы мы знали о действиях, выполняемых сборщиком мусора. Для получения дополнительной информации прочитайте Baeldung-учебник «Мягкие ссылки в Java«, а именно раздел 4.
4.4. Предупреждения об утечке памяти в Eclipse
Для проектов на JDK 1.5 и выше Eclipse выдает предупреждения и ошибки всякий раз, когда сталкивается с очевидными случаями утечки памяти. Поэтому при разработке в Eclipse мы можем регулярно посещать вкладку «Проблемы» и быть более бдительными в отношении предупреждений об утечке памяти (если таковые имеются):
4.5. Бенчмаркинг
Мы можем измерить и проанализировать производительность Java-кода, выполняя эталонные тесты. Таким образом, мы можем сравнить производительность альтернативных подходов к выполнению одной и той же задачи. Это поможет нам выбрать лучший из них и поможет сэкономить память.
Для получения более подробной информации о бенчмаркинге, ознакомьтесь с нашим учебным пособием «Микробенчмаркинг с Java«.
4.6. Обзоры кода
Наконец, у нас всегда есть классический, старый добрый способ — сделать простой обзор кода.
В некоторых случаях даже этот тривиальный на первый взгляд метод может помочь в устранении некоторых распространенных проблем утечки памяти.
5. Заключение
Говоря простым языком, мы можем рассматривать утечку памяти как болезнь, которая снижает производительность нашего приложения, блокируя жизненно важные ресурсы памяти. И, как и все другие болезни, если ее не лечить, со временем она может привести к фатальным сбоям приложения.
Решить проблему утечки памяти непросто, и ее обнаружение требует высокого мастерства и владения языком Java. При борьбе с утечками памяти не существует универсального решения, поскольку они могут возникать из-за множества разнообразных событий.
Однако, если мы будем использовать лучшие практики и регулярно проводить тщательный анализ кода и профилирование, то сможем свести к минимуму риск утечки памяти в нашем приложении.
Фрагменты кода, которые использовались для генерации ответов VisualVM, показанных в этом руководстве, доступны на GitHub.
Материал подготовлен в рамках курса «Нагрузочное тестирование». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.
- java
- утечки памяти
- memory leaks
- нагрузочное тестирование
- load qa
- Блог компании OTUS
- Java
- Тестирование веб-сервисов
Утечки памяти в C++: что это такое и чем они опасны
Разбираемся в трудно уловимых уязвимостях приложений, чтобы всё работало гладко и без тормозов.
Евгений Кучерявый
Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Утечка памяти (англ. memory leak) — это неконтролируемое уменьшение свободной оперативной или виртуальной памяти компьютера. Причиной утечек становятся ошибки в программном коде.
В этой статье мы поймём, как появляются утечки и как их устранять.
Как появляются утечки памяти
Любые программы используют в своей работе память, чтобы хранить какие-то данные. В C++ и многих других языках память динамическая. Это значит, что операционная система при запуске программы резервирует какое-то количество ячеек в ОЗУ, а потом выделяет новые, если они нужны.
Создали переменную для числа (int)? Вот тебе 16 бит (4 байта). Нужен массив из ста элементов для больших чисел (long)? Вот тебе ещё 3200 бит (800 байт).
Когда программисту уже не нужен какой-то массив или объект, он должен сказать системе, что его можно удалить с помощью оператора delete[] и освободить память.
Однако иногда случаются ошибки, которые приводят к утечкам памяти. Вот одна из них:
На примере в цикле десять раз создаётся новый массив, а его адрес записывается в указатель. Адреса старых массивов при этом удаляются. Поэтому дальше оператор delete[] удаляет только последний созданный массив. Остальные останутся в памяти до тех пор, пока не будет закрыта программа.
Чем опасны утечки памяти
Когда приложение съест всю доступную память, сработает защита ОС и ваша программа аварийно закроется. Однако у утечек могут быть и более опасные последствия.
Например, приложение может работать с каким-нибудь файлом непосредственно перед закрытием. В этом случае файл будет повреждён. Последствия возможны самые разные: от нервного срыва пользователя, если это была презентация, над которой он работал несколько дней, до поломки системы, если это был очень важный файл.
В отдельных случаях утечка памяти одного приложения может привести к последствиям для других работающих приложений. Например, если ваш код изменил или занял память, используемую другой программой.
Может показаться, что раз это «утечка», то что-то случится с вашими данными. На самом деле утекает именно свободная память, а не её содержимое.
Как бороться с утечками памяти
Если у вас есть доступ к исходникам, то изучите код, чтобы определить, нет ли там утечек. Вручную делать это бессмысленно, особенно если проект большой, поэтому обратимся к отладчику использования памяти (англ. memory debugger).
Если вы пользуетесь IDE вроде Visual Studio, то там должен быть встроенный отладчик. Есть и сторонние инструменты вроде GDB или LLDB. Отладчик покажет, какие данные хранит программа и к каким ячейкам имеет доступ.
И будьте осторожнее с указателями:
Если доступа к коду у вас нет, но нужна библиотека, в которой есть утечка, то её стоит вынести в отдельную программу (B). Ваша основная программа (A) запустит программу B, чтобы вызвать какую-то функцию. После этого программа B будет закрываться, чтобы освободить всю использованную (в том числе и утёкшую) память.
Если же функционал из библиотеки нужен постоянно, то программу B можно оставить работать, но перезапускать её с определённой периодичностью — до того как она сожрёт слишком большой объём памяти.
Конечно, лучше избегать таких библиотек, но мы живём в неидеальном мире — иногда альтернатив попросту нет.
В более высокоуровневых языках вроде C# или Java существуют сборщики мусора (англ. garbage collector). Это специальный процесс, который сканирует память и удаляет те ячейки, которые уже не нужны приложению.
В C++ сборщика мусора нет, поэтому приходится следить за памятью самостоятельно. Это требует более высокой квалификации разработчика, но позволяет увеличить скорость работы приложений.
Впрочем, иногда от утечек не спасает и сборщик мусора.
Заключение
Такие проблемы всегда сложно искать в коде, поэтому они встречаются так часто и поэтому многие приложения виснут и вылетают.
Читайте также:
- Чем Rust отличается от «плюсов»: откровение ветерана С++
- Как в СССР создавали полупроводниковый компьютер
- Как работает Docker: подробный гайд от техлида
Что такое утечка памяти
Одним из основных преимуществ Java является автоматизированное управление памятью с помощью встроенного сборщика мусора (Garbage Collector или GC). GC неявно заботится о распределении и освобождении памяти и, таким образом, способен обрабатывать большинство проблем, связанных с утечкой памяти.
Хотя сборщик мусора работает достаточно эффективно, но он все равно не может гарантировать стопроцентную защиту от утечек памяти. GC достаточно умен, но не идеален.
Могут возникать ситуации, когда приложение генерирует большое количество лишних объектов, создание которых может привести к истощению ресурсов памяти, а значит и к сбою всего приложения.
Утечки памяти — серьезная проблема в Java. В этом руководстве мы разберем потенциальные причины утечек, посмотрим как распознавать их во время выполнения и как справляться с ними.
2. Что такое утечка памяти?
Утечка памяти — это ситуация, когда в куче есть объекты, которые больше не используются, но сборщик мусора не может удалить их, что приводит к нерациональному расходованию памяти.
Утечка является проблемой, так как она блокирует ресурсы памяти, что со временем приводит к ухудшению производительности системы. И если ее не устранить, приложение исчерпает свои ресурсы и завершиться с ошибкой java.lang.OutOfMemoryError .
Существует два типа объектов, располагающихся в куче: те, которые имеют активные ссылки в приложении и те, на которые ни одна переменная ссылочного типа не ссылается.
Сборщик мусора периодически удаляет объекты на которые не осталось активных ссылок, но никогда не удаляет объекты на которые ссылаются.
Вот, где могут произойти утечки:
Симптомы утечки памяти:
- Серьезное ухудшение производительности, когда оно работает продолжительное время;
- Возникновение в приложении ошибки java.lang.OutOfMemoryError;
- Спонтанные и странные сбои в приложении;
- Иногда в приложении заканчиваются объекты подключения;
3. Типы утечек памяти
В приложениях утечки памяти могут возникать по разным причинам. В этом разделе мы обсудим наиболее распространенные из них.
3.1 Утечки памяти из-за статических полей
Первый сценарий, который может вызвать утечку памяти — это интенсивное использование статических переменных.
В Java время жизни статических полей обычно совпадает со временем работы приложения.
Давайте создадим простую программу которая заполняет статический список (List):
public class StaticTest < public static Listlist = new ArrayList<>(); public void populateList() < for (int i = 0; i < 10000000; i++) < list.add(Math.random()); >Log.info("Debug Point 2"); > public static void main(String[] args) < Log.info("Debug Point 1"); new StaticTest().populateList(); Log.info("Debug Point 3"); >>
Если мы проанализируем память кучи во время выполнения этой программы, то увидим, что между контрольными точками 1 и 2, как и ожидалось, память кучи увеличилась.
Но когда мы остановим метод populateList() в контрольной точке 3, то, как видно из отчета VisualVM, память кучи еще не очищена сборщиком мусора:
Однако, если мы отбросим слово static в строке номер 2, то это приведет к резкому изменению использования памяти:
До первой контрольной точки поведение приложения практически не отличается в обоих случаях. Но во втором случае, после завершения метода populateList(), память была очищена, потому что были удалены все объекты на которых в приложении больше нет активных ссылок.
Следовательно, мы должны быть внимательны при использовании статических переменных. Если коллекции или объекты объявлены как статические, то они остаются в памяти в течение всего срока работы приложения, тем самым блокируя ресурсы, которые можно было бы использовать в другом месте.
Как это предотвратить?
- Минимизировать использование статических переменных в приложении.
- При использовании синглтонов использовать реализацию с ленивый загрузкой объекта, вместо немедленной.
3.2 Через незакрытые ресурсы
Всякий раз, когда мы создаем новое соединение или открываем поток, JVM выделяет память для этих ресурсов. Это могут быть соединения с базой данных, входящие потоки или сессионные объекты.
Забывая закрыть эти ресурсы, вы можете заблокировать память, тем самым делая их недоступными для сборщика мусора. Это может произойти даже в случае возникновения исключения, которое не позволит программе выполнить код, отвечающий за закрытие ресурсов.
В любом случае, открытые соединения потребляеют память и если мы не будем корректно обрабатывать их закрытие, они могут ухудшить производительность системы и даже привести к OutOfMemoryError .
Как это предотвратить?
- Всегда используйте finally блок для закрытия ресурсов.
- Код (даже в блоке finally), который закрывает ресурсы, не должен иметь никаких необработанных исключений.
- При использовании версии Java 7 и выше, мы можем использовать блок try-with-resources.
3.3 Неверные реализации equals() и hashCode()
При написании новых классов очень распространенной ошибкой является некорректное написание переопределяемых методов equals() и hashCode() .
HashSet и HashMap используют эти методы во многих операциях и если они не переопределены правильно, то эти методы могут стать источником потенциальных проблем, связанных с утечкой памяти.
Возьмем для примера простой класс Person и используем его в качестве ключа для HashMap:
public class Person < public String name; public Person(String name) < this.name = name; >>
Теперь вставим дубликаты объектов Person в Map, которая использует их в качестве ключа. Помните, что Map не может содержать дубликаты ключей:
@Test public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() < Mapmap = new HashMap<>(); for(int i = 0; i < 100; i++) < map.put(new Person("jon"), 1); >Assert.assertFalse(map.size() == 1); >
Поскольку Map не позволяет использовать дубликаты ключей, многочисленные объекты Person, которые мы добавили, не должны увеличить занимаемую ими пространство в памяти.
Поскольку мы не определили правильные метод equals(), дублирующие объекты накопились и заняли память. В этом случае потребление памяти кучи выглядит следующим образом:
Однако, если бы мы правильно переопределили методы equals() и hashCode(), тогда в Map существовал бы только один объект Person.
Давайте посмотрим на правильные реализации equals() и hashCode() для нашего класса Person:
public class Person < public String name; public Person(String name) < this.name = name; >@Override public boolean equals(Object o) < if (o == this) return true; if (!(o instanceof Person)) < return false; >Person person = (Person) o; return person.name.equals(name); > @Override public int hashCode() < int result = 17; result = 31 * result + name.hashCode(); return result; >>
И в этом случае наш тест сработает корректно:
@Test public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() < Mapmap = new HashMap<>(); for(int i = 0; i < 2; i++) < map.put(new Person("jon"), 1); >Assert.assertTrue(map.size() == 1); >
После правильного переопределения equals() и hashCode() для класса Person, использование памяти кучи выглядит следующим образом:
Другим примером является использование ORM, например Hibernate, который использует методы equals() и hashCode() для анализа объектов и сохранения их в кеше.
Если эти методы не переопределены, то шансы утечки памяти довольно высоки, потому что Hibernate не сможет сравнивать объекты и заполнит свой кеш их дубликатами.
Как это предотвратить?
- Взять за правило, при создании новых сущностей (Entity), всегда переопределять методы equals() и hashCode() .
- Не достаточно просто переопределить эти методы. Они должны быть переопределены оптимальным образом.
3.4 Внутренние классы, которые ссылаются на внешние классы
Не статическим внутренним классам (анонимным) для инициализации всегда требуется экземпляр внешнего класса.
Каждый нестатический внутренний класс по умолчанию имеет неявную (скрытую) ссылку на класс в котором он находится. Если мы используем этот объект внутреннего класса в нашем приложении, то даже после того, как объект внешнего класса завершает свою работу, он не будет утилизирован сборщиком мусора.
Рассмотрим класс, содержащий ссылку на множество громоздких объектов и имеющий не статический внутренний класс. Теперь, когда мы создаем объект только внутреннего класса, модель памяти выглядит так:
Однако, если мы просто объявим внутренний класс как статический, то та же модель памяти будет выглядеть так:
Это происходит потому, что объект внутреннего класса содержит скрытую ссылку на объект внешнего класса, тем самым делая его недоступным для сборщика мусора. То же самое происходит и в случае анонимных классов.
Как это предотвратить?
- Если внутреннему классу не нужен доступ к членам внешнего класса, подумайте о превращении его в статический класс.
3.5 Через finalize() методы
Использование финализаторов является еще одним потенциальным источником утечек памяти. Всякий раз, когда в классе переопределяется метод finalize(), объект этого класса не убирается сборщиком мусора немедленно. Вместо этого он помещается сборщиком в очередь на утилизацию, которая происходит немного позже.
Кроме того, если код, написанный в методе finalize(), переопределен неоптимально, и если очередь финализатора не может идти в ногу со сборщиком мусора Java, то рано или поздно нашему приложению суждено встретить ошибку OutOfMemoryError.
Пояснение: методы finalize() вызываются последовательно в том порядке, в котором были добавлены в список сборщиком мусора. Соответственно, если какой-то finalize() зависнет, он подвесит поток «Finalizer», но не сборщик мусора. Это в частности означает, что объекты, не имеющие метода finalize(), будут исправно удаляться, а вот имеющие будут добавляться в очередь, пока не отвиснет поток «Finalizer», не завершится приложение или не кончится память.
Чтобы продемонстрировать это, давайте представим, что у нас есть класс, для которого мы переопределили метод finalize() и что для выполнения этого метода требуется немного времени. Когда большое количество объектов этого класса собираются сборщиком мусора, мы видим следующую картину использования памяти кучи:
Однако, если мы просто удалим переопределенный метод finalize(), то эта же программа покажет следующий результат:
Как это предотвратить?
- Мы всегда должны избегать финализаторов.
3.6 Интернированные строки
В Java 7 пул строк претерпел значительные изменения: он был перенесен из PermGen в HeapSpace (подробнее об этом можно прочитать в статье PermGen и Metaspace в среде Java). Но в приложениях, работающих на версии 6 и ниже, мы должны быть более внимательными при работе с большими строкам.
Когда мы читаем большой строковый объект и вызываем у него метод intern(), то он сохраняется в пул строк, который находиться в PermGen (постоянная память) и остается там до тех пор, пока наше приложение работает. Это блокирует память и приводит к серьезным утечкам в приложении.