Время на прочтение
11 мин
Количество просмотров 26K
1. Введение
Одним из основных преимуществ Java является автоматизированное управление памятью с помощью встроенного сборщика мусора (или сокращенно GC). GC неявно заботится о выделении и освобождении памяти и, таким образом, способен решать большинство проблем, связанных с ее утечкой.
Хотя GC эффективно обрабатывает значительную часть памяти, он не гарантирует надежного решения проблемы с ее утечкой. GC достаточно умен, но не безупречен. Утечки памяти все еще могут закрасться даже в приложения, созданные добросовестным разработчиком.
По-прежнему возможны ситуации, когда приложение создает значительное количество лишних объектов, расходуя ресурсы памяти, что иногда приводит к его полному отказу.
Утечки памяти — это настоящая проблема в Java. В этом руководстве мы рассмотрим, каковы потенциальные причины утечек, как распознавать их в рантайме и как справиться с ними в нашем приложении.
2. Что такое утечка памяти
Утечка памяти — это ситуация, когда в куче присутствуют объекты, которые больше не используются, но сборщик мусора не может удалить их из памяти и, таким образом, они сохраняются там без необходимости.
Утечка памяти плоха тем, что она блокирует ресурсы памяти и со временем снижает производительность системы. Если с ней не бороться, приложение в конечном итоге исчерпает свои ресурсы и завершится с фатальной ошибкой java.lang.OutOfMemoryError
.
Существует два различных типа объектов, которые находятся в Heap-памяти (куче) — со ссылками и без них. Объекты со ссылками — это те, на которые имеются активные ссылки внутри приложения, в то время как на другие нет таких ссылок.
Сборщик мусора периодически удаляет объекты без ссылок, но он никогда не собирает объекты, на которые все еще ссылаются. В таких случаях могут возникать утечки памяти:
Признаки утечки памяти
-
Серьезное снижение производительности при длительной непрерывной работе приложения
-
Ошибка кучи
OutOfMemoryError
в приложении -
Спонтанные и странные сбои приложения
-
В приложении время от времени заканчиваются объекты подключения
Давайте подробнее рассмотрим несколько таких сценариев и как с ними бороться.
3. Типы утечек памяти в Java
В любом приложении утечка памяти может произойти по множеству причин. В этом разделе мы обсудим наиболее распространенные из них.
3.1. Утечка памяти через статические поля
Первый сценарий, который может привести к потенциальной утечке памяти, — это интенсивное использование статических переменных.
В Java статические поля имеют срок жизни, который обычно соответствует полному жизненному циклу запущенного приложения (за исключением случаев, когда ClassLoader
получает право на сборку мусора).
Давайте создадим простую Java-программу, которая заполняет статический список:
public class StaticTest {
public static List<Double> list = 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() {
Map<Person, Integer> map = new HashMap<>();
for(int i=0; i<100; i++) {
map.put(new Person("jon"), 1);
}
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() {
Map<Person, Integer> map = new HashMap<>();
for(int i=0; i<2; i++) {
map.put(new Person("jon"), 1);
}
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 {
threadLocal.remove();
}
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.
Материал подготовлен в рамках курса «Нагрузочное тестирование». Если вам интересно узнать подробнее о формате обучения и программе, познакомиться с преподавателем курса — приглашаем на день открытых дверей онлайн. Регистрация здесь.
#статьи
- 29 июн 2020
-
15
Разбираемся в трудно уловимых уязвимостях приложений, чтобы всё работало гладко и без тормозов.
vlada_maestro / shutterstock
Пишет о программировании, в свободное время создаёт игры. Мечтает открыть свою студию и выпускать ламповые RPG.
Утечка памяти (англ. memory leak) — это неконтролируемое уменьшение свободной оперативной или виртуальной памяти компьютера. Причиной утечек становятся ошибки в программном коде.
В этой статье мы поймём, как появляются утечки и как их устранять.
Любые программы используют в своей работе память, чтобы хранить какие-то данные. В C++ и многих других языках память динамическая. Это значит, что операционная система при запуске программы резервирует какое-то количество ячеек в ОЗУ, а потом выделяет новые, если они нужны.
Создали переменную для числа (int)? Вот тебе 16 бит (4 байта). Нужен массив из ста элементов для больших чисел (long)? Вот тебе ещё 3200 бит (800 байт).
Когда программисту уже не нужен какой-то массив или объект, он должен сказать системе, что его можно удалить с помощью оператора delete[] и освободить память.
//Создаём пустой указатель
double *pointer;
//Создаём массив и получаем ссылку на него
pointer = new double[15];
//Указываем тестовое значение для первой ячейки
pointer[0] = 5;
//Выведет адрес ячейки и 5, так как по адресу находится первый элемент массива
cout << "Address: " << pointer << " | Value: " << *pointer << "n";
delete [] pointer;
cout << "Deletedn";
//Адрес остался прежним, но значение будет 0, так как память была очищена
cout << "Address: " << pointer << " | Value: " << *pointer << "n";
Вот результат:
Однако иногда случаются ошибки, которые приводят к утечкам памяти. Вот одна из них:
На примере в цикле десять раз создаётся новый массив, а его адрес записывается в указатель. Адреса старых массивов при этом удаляются. Поэтому дальше оператор delete[] удаляет только последний созданный массив. Остальные останутся в памяти до тех пор, пока не будет закрыта программа.
Когда приложение съест всю доступную память, сработает защита ОС и ваша программа аварийно закроется. Однако у утечек могут быть и более опасные последствия.
Например, приложение может работать с каким-нибудь файлом непосредственно перед закрытием. В этом случае файл будет повреждён. Последствия возможны самые разные: от нервного срыва пользователя, если это была презентация, над которой он работал несколько дней, до поломки системы, если это был очень важный файл.
В отдельных случаях утечка памяти одного приложения может привести к последствиям для других работающих приложений. Например, если ваш код изменил или занял память, используемую другой программой.
Может показаться, что раз это «утечка», то что-то случится с вашими данными. На самом деле утекает именно свободная память, а не её содержимое.
Если у вас есть доступ к исходникам, то изучите код, чтобы определить, нет ли там утечек. Вручную делать это бессмысленно, особенно если проект большой, поэтому обратимся к отладчику использования памяти (англ. memory debugger).
Если вы пользуетесь IDE вроде Visual Studio, то там должен быть встроенный отладчик. Есть и сторонние инструменты вроде GDB или LLDB. Отладчик покажет, какие данные хранит программа и к каким ячейкам имеет доступ.
И будьте осторожнее с указателями:
Если доступа к коду у вас нет, но нужна библиотека, в которой есть утечка, то её стоит вынести в отдельную программу (B). Ваша основная программа (A) запустит программу B, чтобы вызвать какую-то функцию. После этого программа B будет закрываться, чтобы освободить всю использованную (в том числе и утёкшую) память.
Если же функционал из библиотеки нужен постоянно, то программу B можно оставить работать, но перезапускать её с определённой периодичностью — до того как она сожрёт слишком большой объём памяти.
Конечно, лучше избегать таких библиотек, но мы живём в неидеальном мире — иногда альтернатив попросту нет.
В более высокоуровневых языках вроде C# или Java существуют сборщики мусора (англ. garbage collector). Это специальный процесс, который сканирует память и удаляет те ячейки, которые уже не нужны приложению.
В C++ сборщика мусора нет, поэтому приходится следить за памятью самостоятельно. Это требует более высокой квалификации разработчика, но позволяет увеличить скорость работы приложений.
Впрочем, иногда от утечек не спасает и сборщик мусора.
Такие проблемы всегда сложно искать в коде, поэтому они встречаются так часто и поэтому многие приложения виснут и вылетают.
Научитесь: Профессия Разработчик на C++ с нуля
Узнать больше
In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations[1] in a way that memory which is no longer needed is not released. A memory leak may also happen when an object is stored in memory but cannot be accessed by the running code (i.e. unreachable memory).[2] A memory leak has symptoms similar to a number of other problems and generally can only be diagnosed by a programmer with access to the program’s source code.
A related concept is the «space leak», which is when a program consumes excessive memory but does eventually release it.[3]
Because they can exhaust available system memory as an application runs, memory leaks are often the cause of or a contributing factor to software aging.
Consequences[edit]
A memory leak reduces the performance of the computer by reducing the amount of available memory. Eventually, in the worst case, too much of the available memory may become allocated and all or part of the system or device stops working correctly, the application fails, or the system slows down vastly due to thrashing.
Memory leaks may not be serious or even detectable by normal means. In modern operating systems, normal memory used by an application is released when the application terminates. This means that a memory leak in a program that only runs for a short time may not be noticed and is rarely serious.
Much more serious leaks include those:
- where a program runs for a long time and consumes added memory over time, such as background tasks on servers, and especially in embedded systems which may be left running for many years
- where new memory is allocated frequently for one-time tasks, such as when rendering the frames of a computer game or animated video
- where a program can request memory, such as shared memory, that is not released, even when the program terminates
- where memory is very limited, such as in an embedded system or portable device, or where the program requires a very large amount of memory to begin with, leaving little margin for leaks
- where a leak occurs within the operating system or memory manager
- when a system device driver causes a leak
- running on an operating system that does not automatically release memory on program termination.
An example of memory leak[edit]
The following example, written in pseudocode, is intended to show how a memory leak can come about, and its effects, without needing any programming knowledge. The program in this case is part of some very simple software designed to control an elevator. This part of the program is run whenever anyone inside the elevator presses the button for a floor.
When a button is pressed: Get some memory, which will be used to remember the floor number Put the floor number into the memory Are we already on the target floor? If so, we have nothing to do: finished Otherwise: Wait until the lift is idle Go to the required floor Release the memory we used to remember the floor number
The memory leak would occur if the floor number requested is the same floor that the elevator is on; the condition for releasing the memory would be skipped. Each time this case occurs, more memory is leaked.
Cases like this would not usually have any immediate effects. People do not often press the button for the floor they are already on, and in any case, the elevator might have enough spare memory that this could happen hundreds or thousands of times. However, the elevator will eventually run out of memory. This could take months or years, so it might not be discovered despite thorough testing.
The consequences would be unpleasant; at the very least, the elevator would stop responding to requests to move to another floor (such as when an attempt is made to call the elevator or when someone is inside and presses the floor buttons). If other parts of the program need memory (a part assigned to open and close the door, for example), then no one would be able to enter, and if someone happens to be inside, they will become trapped (assuming the doors cannot be opened manually).
The memory leak lasts until the system is reset. For example: if the elevator’s power were turned off or in a power outage, the program would stop running. When power was turned on again, the program would restart and all the memory would be available again, but the slow process of memory leak would restart together with the program, eventually prejudicing the correct running of the system.
The leak in the above example can be corrected by bringing the ‘release’ operation outside of the conditional:
When a button is pressed: Get some memory, which will be used to remember the floor number Put the floor number into the memory Are we already on the target floor? If not: Wait until the lift is idle Go to the required floor Release the memory we used to remember the floor number
Programming issues[edit]
Memory leaks are a common error in programming, especially when using languages that have no built in automatic garbage collection, such as C and C++. Typically, a memory leak occurs because dynamically allocated memory has become unreachable. The prevalence of memory leak bugs has led to the development of a number of debugging tools to detect unreachable memory. BoundsChecker, Deleaker, Memory Validator, IBM Rational Purify, Valgrind, Parasoft Insure++, Dr. Memory and memwatch are some of the more popular memory debuggers for C and C++ programs. «Conservative» garbage collection capabilities can be added to any programming language that lacks it as a built-in feature, and libraries for doing this are available for C and C++ programs. A conservative collector finds and reclaims most, but not all, unreachable memory.
Although the memory manager can recover unreachable memory, it cannot free memory that is still reachable and therefore potentially still useful. Modern memory managers therefore provide techniques for programmers to semantically mark memory with varying levels of usefulness, which correspond to varying levels of reachability. The memory manager does not free an object that is strongly reachable. An object is strongly reachable if it is reachable either directly by a strong reference or indirectly by a chain of strong references. (A strong reference is a reference that, unlike a weak reference, prevents an object from being garbage collected.) To prevent this, the developer is responsible for cleaning up references after use, typically by setting the reference to null once it is no longer needed and, if necessary, by deregistering any event listeners that maintain strong references to the object.
In general, automatic memory management is more robust and convenient for developers, as they do not need to implement freeing routines or worry about the sequence in which cleanup is performed or be concerned about whether or not an object is still referenced. It is easier for a programmer to know when a reference is no longer needed than to know when an object is no longer referenced. However, automatic memory management can impose a performance overhead, and it does not eliminate all of the programming errors that cause memory leaks.
RAII[edit]
Resource acquisition is initialization (RAII) is an approach to the problem commonly taken in C++, D, and Ada. It involves associating scoped objects with the acquired resources, and automatically releasing the resources once the objects are out of scope. Unlike garbage collection, RAII has the advantage of knowing when objects exist and when they do not. Compare the following C and C++ examples:
/* C version */ #include <stdlib.h> void f(int n) { int* array = calloc(n, sizeof(int)); do_some_work(array); free(array); }
// C++ version #include <vector> void f(int n) { std::vector<int> array (n); do_some_work(array); }
The C version, as implemented in the example, requires explicit deallocation; the array is dynamically allocated (from the heap in most C implementations), and continues to exist until explicitly freed.
The C++ version requires no explicit deallocation; it will always occur automatically as soon as the object array
goes out of scope, including if an exception is thrown. This avoids some of the overhead of garbage collection schemes. And because object destructors can free resources other than memory, RAII helps to prevent the leaking of input and output resources accessed through a handle, which mark-and-sweep garbage collection does not handle gracefully. These include open files, open windows, user notifications, objects in a graphics drawing library, thread synchronisation primitives such as critical sections, network connections, and connections to the Windows Registry or another database.
However, using RAII correctly is not always easy and has its own pitfalls. For instance, if one is not careful, it is possible to create dangling pointers (or references) by returning data by reference, only to have that data be deleted when its containing object goes out of scope.
D uses a combination of RAII and garbage collection, employing automatic destruction when it is clear that an object cannot be accessed outside its original scope, and garbage collection otherwise.
Reference counting and cyclic references[edit]
More modern garbage collection schemes are often based on a notion of reachability – if you do not have a usable reference to the memory in question, it can be collected. Other garbage collection schemes can be based on reference counting, where an object is responsible for keeping track of how many references are pointing to it. If the number goes down to zero, the object is expected to release itself and allow its memory to be reclaimed. The flaw with this model is that it does not cope with cyclic references, and this is why nowadays most programmers are prepared to accept the burden of the more costly mark and sweep type of systems.
The following Visual Basic code illustrates the canonical reference-counting memory leak:
Dim A, B Set A = CreateObject("Some.Thing") Set B = CreateObject("Some.Thing") ' At this point, the two objects each have one reference, Set A.member = B Set B.member = A ' Now they each have two references. Set A = Nothing ' You could still get out of it... Set B = Nothing ' And now you've got a memory leak! End
In practice, this trivial example would be spotted straight away and fixed. In most real examples, the cycle of references spans more than two objects, and is more difficult to detect.
A well-known example of this kind of leak came to prominence with the rise of AJAX programming techniques in web browsers in the lapsed listener problem. JavaScript code which associated a DOM element with an event handler, and failed to remove the reference before exiting, would leak memory (AJAX web pages keep a given DOM alive for a lot longer than traditional web pages, so this leak was much more apparent).
Effects[edit]
If a program has a memory leak and its memory usage is steadily increasing, there will not usually be an immediate symptom. Every physical system has a finite amount of memory, and if the memory leak is not contained (for example, by restarting the leaking program) it will eventually cause problems.
Most modern consumer desktop operating systems have both main memory which is physically housed in RAM microchips, and secondary storage such as a hard drive. Memory allocation is dynamic – each process gets as much memory as it requests. Active pages are transferred into main memory for fast access; inactive pages are pushed out to secondary storage to make room, as needed. When a single process starts consuming a large amount of memory, it usually occupies more and more of main memory, pushing other programs out to secondary storage – usually significantly slowing performance of the system. Even if the leaking program is terminated, it may take some time for other programs to swap back into main memory, and for performance to return to normal.
When all the memory on a system is exhausted (whether there is virtual memory or only main memory, such as on an embedded system) any attempt to allocate more memory will fail. This usually causes the program attempting to allocate the memory to terminate itself, or to generate a segmentation fault. Some programs are designed to recover from this situation (possibly by falling back on pre-reserved memory). The first program to experience the out-of-memory may or may not be the program that has the memory leak.
Some multi-tasking operating systems have special mechanisms to deal with an out-of-memory condition, such as killing processes at random (which may affect «innocent» processes), or killing the largest process in memory (which presumably is the one causing the problem). Some operating systems have a per-process memory limit, to prevent any one program from hogging all of the memory on the system. The disadvantage to this arrangement is that the operating system sometimes must be re-configured to allow proper operation of programs that legitimately require large amounts of memory, such as those dealing with graphics, video, or scientific calculations.
The «sawtooth» pattern of memory utilization: the sudden drop in used memory is a candidate symptom for a memory leak.
If the memory leak is in the kernel, the operating system itself will likely fail. Computers without sophisticated memory management, such as embedded systems, may also completely fail from a persistent memory leak.
Publicly accessible systems such as web servers or routers are prone to denial-of-service attacks if an attacker discovers a sequence of operations which can trigger a leak. Such a sequence is known as an exploit.
A «sawtooth» pattern of memory utilization may be an indicator of a memory leak within an application, particularly if the vertical drops coincide with reboots or restarts of that application. Care should be taken though because garbage collection points could also cause such a pattern and would show a healthy usage of the heap.
Other memory consumers[edit]
Note that constantly increasing memory usage is not necessarily evidence of a memory leak. Some applications will store ever increasing amounts of information in memory (e.g. as a cache). If the cache can grow so large as to cause problems, this may be a programming or design error, but is not a memory leak as the information remains nominally in use. In other cases, programs may require an unreasonably large amount of memory because the programmer has assumed memory is always sufficient for a particular task; for example, a graphics file processor might start by reading the entire contents of an image file and storing it all into memory, something that is not viable where a very large image exceeds available memory.
To put it another way, a memory leak arises from a particular kind of programming error, and without access to the program code, someone seeing symptoms can only guess that there might be a memory leak. It would be better to use terms such as «constantly increasing memory use» where no such inside knowledge exists.
A simple example in C++[edit]
The following C++ program deliberately leaks memory by losing the pointer to the allocated memory.
int main() { int* a = new int(5); a = nullptr; /* The pointer in the 'a' no longer exists, and therefore cannot be freed, but the memory is still allocated by the system. If the program continues to create such pointers without freeing them, it will consume memory continuously. Therefore, a leak would occur. */ }
See also[edit]
- Buffer overflow
- Memory management
- Memory debugger
- Plumbr is a popular memory leak detection tool for applications running on Java Virtual Machine.
- nmon (short for Nigel’s Monitor) is a popular system monitor tool for the AIX and Linux operating systems.
References[edit]
- ^ Crockford, Douglas. «JScript Memory Leaks». Archived from the original on 7 December 2012. Retrieved 20 July 2022.
- ^ «Creating a memory leak with Java». Stack Overflow. Retrieved 2013-06-14.
- ^ Mitchell, Neil. «Leaking Space». Retrieved 27 May 2017.
External links[edit]
- Visual Leak Detector Archived 2015-12-15 at the Wayback Machine for Visual Studio, open source
- Valgrind, open source
- Deleaker for Visual Studio, proprietary
- Memory Validator for Visual Studio, Delphi, Fortran, Visual Basic. Proprietary.
- Detecting a Memory Leak (Using MFC Debugging Support)
- Article «Memory Leak Detection in Embedded Systems» by Cal Erickson
- WonderLeak, a high performance Windows heap and handle allocation profiler, proprietary
Утечка памяти – это неправильное размещение ресурса в компьютерной программе из-за неправильного распределения памяти. Это происходит, когда неиспользуемая область ОЗУ остается невыпущенной. Утечку памяти не следует путать с утечкой пространства, которая относится к программе, использующей больше оперативной памяти, чем необходимо. Утечка памяти в системе Windows 10/8/7, как говорят, произошла, когда память просто недоступна, несмотря на то, что она не используется.
Содержание
- Утечки памяти в Windows 10
- Предотвращение утечек памяти
- Устранение утечек памяти в Windows
Утечки памяти в Windows 10
Прежде чем начать, вы должны знать, что утечка памяти – это проблема программного обеспечения для отладки – например, в Java, JavaScript, C/C ++, Windows и т. Д. Физическая замена ОЗУ или жесткого диска не требуется.
Почему это плохо
Очевидно, что утечка памяти – это плохо, потому что это ошибка, недостаток в системе. Но давайте выясним, как именно это влияет на систему:
- Поскольку память не освобождается, даже когда она не используется, это приводит к ее истощению.
- Исчерпание памяти приводит к старению программного обеспечения.
- Уменьшение доступной памяти приводит к увеличению времени отклика и снижению производительности системы.
- Неконтролируемая утечка памяти может в конечном итоге привести к сбою приложения.
Чтобы идентифицировать утечку памяти, программист должен иметь доступ к исходному коду программы.
Обнаружение утечки
Чтобы решить проблему, нам нужно сначала ее идентифицировать. Основные шаги по обнаружению утечки памяти:
- Подтверждение . Определение наличия утечки.
- Поиск утечки памяти в режиме ядра . Поиск утечки, вызванной компонентом драйвера режима ядра.
- Поиск утечки памяти в пользовательском режиме . Поиск утечки, вызванной драйвером пользовательского режима или приложением.
Распределение памяти
Существуют разные режимы, в которых приложения выделяют оперативную память. Если пространство не освобождается после использования, утечка памяти будет происходить независимо от режима выделения. Некоторые общие шаблоны распределения:
- Функция HealAlloc для выделения кучи памяти. Эквивалентами времени выполнения C/C ++ являются malloc и новые.
- Функция VirtualAlloc для прямого выделения из ОС.
- Kernel32 API для хранения памяти ядра для приложения. Пример, CreateFile, CreateThread.
- User32 API и Gdi32 API.
Предотвращение утечек памяти
Мы все знаем, что профилактика лучше лечения, поэтому есть несколько способов предотвратить утечку памяти.
Мониторинг привычек
Вы должны следить за ненормальным использованием ОЗУ отдельными программами и приложениями. Вы можете перейти в диспетчер задач Windows, нажав CTRL + SHIFT + ESC и добавить такие столбцы, как дескрипторы, объекты пользователя, объекты GDI и т. Д.
Это поможет вам легко отслеживать использование ресурсов.
Инструменты Microsoft для диагностики утечек памяти
Различные инструменты диагностируют утечки памяти для различных режимов выделения:
- Верификатор приложения диагностирует утечки кучи.
- UMDH (компонент средств отладки Windows) диагностирует утечки для отдельных процессов, отслеживая выделение кучи памяти.
- Trace Capture для тщательного анализа использования оперативной памяти.
- Xperf также отслеживает шаблоны распределения кучи.
- CRT Debug Heap не только отслеживает выделение кучи, но также позволяет использовать методы кодирования для минимизации утечек.
- JavaScript Memory Leak Detector отлаживает утечки памяти в кодах.
Советы по использованию
- Используйте ядра HANDLE и другие умные указатели для ресурсов Win32 и выделения кучи.
- Получите классы для автоматического управления ресурсами для выделения ядра из библиотеки ATL. Стандарт C ++ имеет auto_ptr для распределения кучи.
- Инкапсулируйте указатели COM-интерфейса в «умные указатели» с помощью _com_ptr_t или _bstr_t или _variant_t .
- Мониторинг кода .NET на предмет ненормального использования памяти.
- Избегайте множественных путей выхода для функций, чтобы к концу функции освободить выделения из переменных в большинстве блоков.
- Используйте собственные исключения только после освобождения всех выделений в блоке _finally. Оберните всю кучу и обработайте выделения в интеллектуальные указатели, чтобы использовать исключения C ++.
- Всегда вызывайте функцию PropVariantClear перед повторной инициализацией или удалением объекта PROPVARIANT.
Устранение утечек памяти в Windows
Так же, как различные способы предотвращения утечек памяти, существуют различные способы остановить утечки памяти.
1] Закройте процессы и перезапустите.
Если вы видите, что ненужный процесс занимает слишком много ОЗУ, вы можете завершить процесс в диспетчере задач. Вам нужно будет перезагрузить устройство, чтобы освободившееся пространство было доступно для использования другими процессами. Без перезагрузки проблема утечки памяти не будет решена. Одним из конкретных процессов, которые имеют ошибки для замедления работы ПК, является Runtime Broker. Попробуйте, если отключение, которое само по себе работает.
2] Инструменты диагностики памяти
Чтобы получить доступ к встроенному инструменту диагностики памяти для Windows:
- Сохраните всю вашу важную работу.
- Нажмите Win + R , чтобы открыть окно Выполнить .
- Введите команду mdsched.exe в окне Выполнить .
- Перезагрузите компьютер.
- После перезапуска выполните базовое сканирование или выберите параметры Расширенные , например Test mix ’или Количество проходов ’.
- Нажмите F10 , чтобы начать тестирование.
Это все еще временные исправления.
3] Проверить обновления драйверов
Устаревшие драйверы вызывают утечки памяти. Держите все драйверы обновленными:
- Нажмите Win + R и откройте окно Выполнить . Введите devmgmt.msc и нажмите Enter. Вы попадете в Диспетчер устройств .
- Проверьте устаревшие драйверы и обновите их все.
- Для обновлений, которые вы могли пропустить, проверьте в Центре обновления Windows.
Это было просто.
4] Оптимизация производительности
Настройка Windows на производительность будет управлять всем, включая планирование процессора и использование памяти, чтобы предотвратить утечки памяти. Следуй этим шагам:
- Нажмите правой кнопкой мыши на Этот компьютер ’и выберите настройки Дополнительно на левой панели.
- На вкладке “ Дополнительно ” перейдите в раздел “ Эффективность “, а затем “ Настройки “.
- Установите флажок Настроить для лучшей производительности и нажмите ОК .
- Перезапустите и проверьте, решена ли проблема.
Если это простое решение не сработало, попробуйте следующее решение.
5] Отключить программы, запускаемые при запуске
Отключение проблемных программ – единственный способ избавиться от проблемы утечки памяти. Зайдите в диспетчер задач и отключите программу, создающую проблемы. Если вы не знаете, какие программы создают проблемы, сделайте следующее:
- Перейдите в Диспетчер задач .
- Перейдите в “ Запуск “.
- Отключите автозапуск программ, которые вам не нужно запускать по умолчанию.
6] Дефрагментация жесткого диска
Хотя Windows 10 делает это для вас автоматически, вам может понадобиться время от времени выполнять дефрагментацию жестких дисков для оптимизации производительности:
- Перейдите на страницу “ Этот компьютер ” или “ Мой компьютер “.
- Щелкните правой кнопкой мыши системный жесткий диск (обычно диск C:).
- Перейдите на вкладку Инструменты и выберите Свойства ‘и выберите Оптимизировать ’.
- Выберите диск для дефрагментации и выберите « Анализировать ».
Перезагрузите компьютер после новой фрагментации.
7] Файл ClearPage при завершении работы
Сейчас становится все сложнее, но не волнуйтесь. Вот как очищать файл подкачки при каждом выключении:
- Введите regedit в поле поиска, чтобы запустить редактор реестра.
- Введите этот путь: HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControlSession ManagerMemory Management
- Измените значение ClearPageFileAtShutDown на «1».
- Сохраните изменения и перезагрузите устройство.
Это должно сделать это.
9] Отключить суперпатч
Этот сервис Windows оптимизирует производительность за счет минимизации времени загрузки. Это позволяет Windows управлять использованием оперативной памяти. Жизнь после отключения Superfetch не удобна, но сделайте это, если нужно. По крайней мере, попробуйте это в одиночку, чтобы изолировать проблему:
- Найдите services.msc и перейдите в диспетчер служб.
- Найдите Superfetch и нажмите его правой кнопкой мыши, чтобы перейти в Свойства .
- Выберите « Стоп ».
- Также Отключить ’сервис из раскрывающегося меню.
- Перезагрузите компьютер и проверьте, не улучшилась ли производительность.
Включите Superfetch, если этот не работает.
10] Проверка на наличие вредоносных программ
Используйте стороннее антивирусное программное обеспечение или встроенный в Windows 10 Защитник Windows для сканирования и устранения вредоносных программ.Убедитесь, что антивирус обновлен для поддержки вашей ОС, чтобы он не стал причиной утечки памяти.
Каждое решение, которое вам когда-либо понадобится, чтобы найти или предотвратить утечку памяти, находится здесь. Вы можете прочитать больше об утечках памяти на MSDN и Microsoft.
- →
Одним из основных преимуществ Java является автоматизированное управление памятью с помощью встроенного сборщика мусора (Garbage Collector или GC). GC неявно заботится о распределении и освобождении памяти и, таким образом, способен обрабатывать большинство проблем, связанных с утечкой памяти.
Хотя сборщик мусора работает достаточно эффективно, но он все равно не может гарантировать стопроцентную защиту от утечек памяти. GC достаточно умен, но не идеален.
Могут возникать ситуации, когда приложение генерирует большое количество лишних объектов, создание которых может привести к истощению ресурсов памяти, а значит и к сбою всего приложения.
Утечки памяти — серьезная проблема в Java. В этом руководстве мы разберем потенциальные причины утечек, посмотрим как распознавать их во время выполнения и как справляться с ними.
2. Что такое утечка памяти?
Утечка памяти — это ситуация, когда в куче есть объекты, которые больше не используются, но сборщик мусора не может удалить их, что приводит к нерациональному расходованию памяти.
Утечка является проблемой, так как она блокирует ресурсы памяти, что со временем приводит к ухудшению производительности системы. И если ее не устранить, приложение исчерпает свои ресурсы и завершиться с ошибкой java.lang.OutOfMemoryError.
Существует два типа объектов, располагающихся в куче: те, которые имеют активные ссылки в приложении и те, на которые ни одна переменная ссылочного типа не ссылается.
Сборщик мусора периодически удаляет объекты на которые не осталось активных ссылок, но никогда не удаляет объекты на которые ссылаются.
Вот, где могут произойти утечки:
Симптомы утечки памяти:
- Серьезное ухудшение производительности, когда оно работает продолжительное время;
- Возникновение в приложении ошибки java.lang.OutOfMemoryError;
- Спонтанные и странные сбои в приложении;
- Иногда в приложении заканчиваются объекты подключения;
Давайте подробнее рассмотрим некоторые из этих симптомов и способы их устранения.
В приложениях утечки памяти могут возникать по разным причинам. В этом разделе мы обсудим наиболее распространенные из них.
3.1 Утечки памяти из-за статических полей
Первый сценарий, который может вызвать утечку памяти — это интенсивное использование статических переменных.
В Java время жизни статических полей обычно совпадает со временем работы приложения.
Давайте создадим простую программу которая заполняет статический список (List):
public class StaticTest {
public static List<Double> list = 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() {
Map<Person, Integer> map = 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() {
Map<Person, Integer> map = 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 (постоянная память) и остается там до тех пор, пока наше приложение работает. Это блокирует память и приводит к серьезным утечкам в приложении.
В таком случае использование PermGen пространства в JVM 1.6 выглядит следующим образом:
В том случае, если мы просто читаем строку из файла и не интернируем ее, то PermGen выглядит так:
Как это предотвратить?
- Самый простой способ решить эту проблему — обновиться до последней версии Java, поскольку пул строк был перемещен в пространство кучи, начиная с 7 версии.
- При работе с большими строками можно увеличить размер PermGen, что позволит избежать ошибки OutOfMemoryError:
3.7 Использование ThreadLocals
ThreadLocal — это механизм, который позволяет изолировать состояние (значения переменных) в определенном потоке, что делает его безопасным.
При использовании этой конструкции, каждый поток будет содержать неявную ссылку на его копию переменной ThreadLocal и будет хранить свою собственную копию, вместо того чтобы совместно использовать ресурс через множество потоков, так долго, сколько поток будет жить.
Несмотря на свои преимущества, использование переменных ThreadLocal противоречиво, поскольку они могут являться причиной утечек памяти, если они не используются должным образом.
Утечки памяти по причине использования ThreadLocals.
Предполагается что ThreadLocal переменные будут собраны сборщиком мусора после того, как содержащий их поток перестанет существовать. Но существует проблема с использованием ThreadLocal в современных серверах приложений.
Современные сервера приложений используют пул потоков для обработки запросов, вместо создания нового потока на каждый запрос. Кроме того, они используют отдельный загрузчик классов.
Поскольку пулы потоков в серверах приложений работают по принципу повторного использования потоков, они никогда не удаляются сборщиком мусора — вместо этого они повторно используются для обслуживания другого запроса.
Итак, если класс создает ThreadLocal переменную, но не удаляет ее явно, то копия этого объекта останется в рабочем потоке даже после остановки веб-приложения, тем самым не позволяя сборщику удалить этот объект.
Как это предотвратить?
- Хорошей практикой является очищение ThreadLocal переменных, когда они больше не используются. ThreadLocal предоставляет метод remove(), который удаляет значение переменной для текущего потока
- Не используйте ThreadLocal. set (null) для очистки значения — на самом деле оно не очищает значение, а вместо этого ищет мапу, связанную с текущим потоком, и устанавливает пару ключ-значение — текущий поток и null соответственно
- Еще лучше рассмотреть ThreadLocal как ресурс, который необходимо закрыть в блоке finally, чтобы убедиться, что он всегда будет закрыт, даже в случае исключения:
try {
threadLocal.set(System.nanoTime());
//... further processing
} finally {
threadLocal.remove();
}
4. Другие стратегии для борьбы с утечками памяти
Несмотря на то, что при работе с утечками памяти нет единого решения для всех случаев, есть некоторые способы, с помощью которых мы можем минимизировать эти утечки.
4.1 Включить профилирование
Java-профайлеры — это инструменты, которые контролируют и диагностируют утечки памяти. Они анализируют, что происходит внутри нашего приложения, например, как распределяется память.
Используя профайлеры, мы можем сравнивать различные подходы и находить области, где мы можем оптимально использовать наши ресурсы.
4.2 Детальная сборка мусора
Включая режим детальной сборки мусора мы можем отслеживать подробности, происходящие при работе GC. Чтобы включить этот функционал, нужно добавить следующую настройку JVM:
Включив этот параметр, мы увидим детали того, что происходит внутри сборщика мусора:
4.3 Используйте ссылочные объекты, чтобы избежать утечек памяти
Мы также можем прибегнуть к ссылочным объектам в Java, которые встроены в пакет java.lang.ref для устранения утечек памяти. Используя пакет java.lang.ref, вместо прямых ссылки на объекты, мы используем специальные ссылки на объекты, которые способствуют легкому удалению сборщиком мусора этих объектов.
Мы можем измерять и анализировать производительность Java-кода, выполняя тесты. Таким образом, мы можем сравнить эффективность альтернативных подходов для выполнения одной и той же задачи. Это может помочь нам выбрать лучший подход и сохранить память.
Наконец, у нас есть классический способ — просто, лишний раз пробежаться по нашему коду.
В некоторых случаях даже этот тривиальный метод может помочь в устранении некоторых распространенных проблем утечки памяти.
С точки зрения непрофессионала мы можем думать об утечке памяти, как о болезни, которая ухудшает производительность нашего приложения, блокируя жизненно важные ресурсы памяти. И, как и все другие болезни, со временем, может привести к летальному исходу приложения.
Утечки памяти не просто исправить — их поиск требует высокого мастерства владения языком Java. При работе с утечками памяти нет единого решения для всех случаев, так как утечки могут возникать в результате широкого спектра разнообразных событий.
Однако, если мы прибегаем к передовым практикам, регулярно проверяем наш код и используем профилирование, мы можем свести к минимуму риск утечек памяти в нашем приложении.
Оцените статью, если она вам понравилась!