Разбираемся с утечками памяти в Java (Memory Leak) в одной статье.
Разбираемся с утечками памяти в Java (Memory Leak) в одной статье.

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

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

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

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

Прежде чем анализировать утечку памяти, давайте сначала проясним соответствующие концепции. Утечка памяти и OutOfMemoryError. Утечку памяти можно считать проблемой, а OutOfMemoryError — симптомом. Таким образом, не все ошибки OutOfMemoryErrors означают утечки памяти, и не все утечки памяти проявляются как ошибки OutOfMemoryErrors.

Что такое утечка памяти в Java?

Утечка памяти, или «утечка памяти», обычно относится к ситуации, когда один или несколько объектов больше не используются, но при этом не могут быть очищены продолжающим работать сборщиком мусора.

Мы можем разделить объекты в памяти на две основные категории:

1. Ссылочный объект — это объект, доступный из кода нашего приложения и который используется или будет использоваться.

2. Объекты, на которые нет ссылок, — это объекты, к которым невозможно получить доступ из кода приложения.

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

Вообще говоря, утечки памяти — это плохо в реальных бизнес-сценариях, независимо от того, основаны ли они на производительности бизнеса или пользовательском опыте, поскольку они блокируют ресурсы памяти и со временем приводят к снижению производительности системы. Если не принять меры незамедлительно, приложение в конечном итоге исчерпает свои ресурсы и в конечном итоге закроется с фатальным прерыванием Java.lang.OutOfMemoryError.

В модели памяти Java существует два разных типа объектов, которые находятся в куче памяти: «на которые есть ссылки» и «на которые нет ссылок». Объекты, на которые имеются ссылки, — это объекты, которые все еще имеют активные ссылки в приложении, тогда как объекты, на которые нет ссылок, не имеют активных ссылок.

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

Симптомы утечки памяти

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

1. Ошибка Java OutOfMemory возникает при запуске приложения. ‍

2. Производительность снижается при длительной работе приложения и проявляется не сразу после запуска приложения.

3. Чем дольше работает приложение, тем больше выполняется сборка мусора.

4. Соединение исчерпано.

Why Memory Leak ?‍

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

В некоторых конкретных сценариях это верно, даже если указан метод System.gc(). Сборщик мусора, скорее всего, запустится, когда не хватает памяти или недостаточно доступной памяти для поддержки нужд программы. Если сборщик мусора не освободит достаточно ресурсов памяти, приложение будет использовать память операционной системы.

Утечки памяти в Java обычно менее серьезны, чем утечки памяти в C++ и других языках программирования. По словам Джима Патрика из IBM DeveloperWorks, при рассмотрении утечек памяти следует учитывать два аспекта:

1. Размер утечки

2. Жизненный цикл программы

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

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

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

Практический сценарий утечки памяти‍‍‍‍

1. Статические поля содержат объекты

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

Вот простой пример кода, демонстрирующий такое поведение:

Язык кода:javascript
копировать
public class StaticReferenceLeak {
  public static List<Integer> NUMBERS = new ArrayList<>();
  public void addBatch() {
    for (int i = 0; i < 100000; i++) {
      NUMBERS.add(i);
    }
  }
  public static void main(String[] args) throws Exception {
    for (int i = 0; i < 1000000; i++) {
        (new StaticReferenceLeak()).addBatch();
        System.gc();
        Thread.sleep(10000);
    }
  }
}

Метод addBatch добавляет 100 000 целых чисел в коллекцию с именем NUMBERS. Конечно, если нам нужны данные, это совершенно нормально. Но в этом случае мы никогда его не удаляем. Несмотря на то, что мы создали объект StaticReferenceLeak в основном методе и не сохранили ссылку на него, мы легко видим, что сборщик мусора не может очистить память. Вместо этого он продолжает расти:

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

Решение:

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

2. Незакрытые ресурсы

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

Чтобы проиллюстрировать эту проблему, давайте рассмотрим следующий пример:

Язык кода:javascript
копировать
public class UnclosedResources {
  public static void main(String[] args) throws Exception {
    for (int i = 0; i < 1000000; i++) {
      URL url = new URL("http://www.google.com");
      URLConnection conn = url.openConnection();
      InputStream is = conn.getInputStream();
      // rest of the code goes here
    }
  }
}

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

Решение:

(1) Всегда используйте блокиfinally для закрытия ресурсов‍

(2) Сам код, закрывающий ресурс (даже в блокеfinally), не должен иметь никаких исключений‍

(3) При использовании Java 7+ мы можем использовать блок try -with-resources.

3. Используйте ThreadLocals

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

ThreadLocal (подробно обсуждается в руководстве «Введение в ThreadLocal в Java») — это конструкция, которая позволяет нам изолировать состояние конкретного потока, что позволяет нам достичь потокобезопасности.

При использовании этой конструкции каждый поток будет хранить неявную ссылку на свою копию переменной ThreadLocal и поддерживать свою собственную копию, а не совместно использовать ресурс между несколькими потоками, пока поток активен.

Несмотря на многочисленные преимущества, использование переменных ThreadLocal вызывает споры, поскольку они печально известны тем, что при неправильном использовании приводят к утечкам памяти. Джошуа Блох однажды прокомментировал использование локализации потоков:

“Sloppy use of thread pools in combination with sloppy use of thread locals can cause unintended object retention, as has been noted in many places. But placing the blame on thread locals is unwarranted.”

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

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

Решение:

(1) Рекомендуется очищать ThreadLocals, когда мы их больше не используем. ThreadLocals предоставляет метод Remove(), который удаляет значение этой переменной из текущего потока.

(2) Не используйте ThreadLocal.set(null) для очистки значения. На самом деле он не очищает значение, а вместо этого просматривает карту, связанную с текущим потоком, и устанавливает для пары ключ-значение значение текущего потока и значение Null соответственно.

(3) Лучше всего думать о ThreadLocal как о ресурсе, который нам нужно закрыть в блокеfinally, даже в случае исключения:

Язык кода:javascript
копировать
try {
    threadLocal.set(System.nanoTime());
    //... further processing
}
finally {
    threadLocal.remove();
}

4. Внутренние классы, ссылающиеся на внешние классы.

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

Язык кода:javascript
копировать
public class OuterClass {
  // some large arrays of values
  private InnerClass inner;
  public void create() {
    inner = new InnerClass();
    // do something with inner and keep it
  }
  class InnerClass {
    // some logic of the inner class
  }
}

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

Решение:

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

5. Используйте неправильную реализацию методаquals() и hashCode().

Другим распространенным примером утечек памяти Java является использование объектов с пользовательскими методами Equals() и hashCode(), которые реализованы неправильно (или вообще не существуют), а также коллекций, которые используют хэши для проверки на наличие дубликатов. Типичным представителем такого рода наборов является HashSet.

Чтобы проиллюстрировать эту проблему, давайте рассмотрим следующий пример:

Язык кода:javascript
копировать
public class HashAndEqualsNotImplemented {
  public static void main(String[] args) {
    Set<Entry> set = new HashSet<>();
    for (int i = 0; i < 1000; i++) {
      set.add(new Entry("test"));
    }
    System.out.println(set.size());
  }
}
class Entry {
  public String entry;
  public Entry(String entry) {
    this.entry = entry;
  }
}

Прежде чем мы углубимся в объяснение, задайте себе простой вопрос: какое число ваш код напечатает с помощью вызова System.out.println(set.size())? Если ответ 1000, то да будет правильным. Это потому, что мы неправильно реализовали метод равенства. Это означает, что каждый экземпляр объекта Entry, добавленный в HashSet, будет добавлен независимо от того, является ли он дубликатом с нашей точки зрения. Это может вызвать исключение OutOfMemory.

Если мы изменим наш код на правильную реализацию, код приведет к печати 1 как размера нашего HashSet. Давайте возьмем следующий сценарий в качестве простого примера. Ниже приведен код методовquals() и hashCode(), реализованных JetBrains IntelliJ:

Язык кода:javascript
копировать
public class HashAndEqualsNotImplemented {
  public static void main(String[] args) {
    Set<Entry> set = new HashSet<>();
    for (int i = 0; i < 1000; i++) {
      set.add(new Entry("test"));
    }
    System.out.println(set.size());
  }
}
class Entry {
  public String entry;
  public Entry(String entry) {
    this.entry = entry;
  }
  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Entry entry1 = (Entry) o;
    return Objects.equals(entry, entry1.entry);
  }
  @Override
  public int hashCode() {
    return Objects.hash(entry);
  }
}

Решение:

Как правило, методы Equals() и hashCode() должны быть правильно реализованы при создании класса. Большинство современных IDE помогут нам оптимизировать это.

6. Используйте метод Finalize().

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

Более того, если код, написанный в методе Finalize(), неоптимален и если очередь финализатора не успевает за сборщиком мусора Java, рано или поздно нашему приложению суждено столкнуться с ошибкой OutOfMemoryError.

Решение:

Проще говоря, отключите этот метод.

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

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

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

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

Adiós !

boy illustration
Неразрушающее увеличение изображений одним щелчком мыши, чтобы сделать их более четкими артефактами искусственного интеллекта, включая руководства по установке и использованию.
boy illustration
Копикодер: этот инструмент отлично работает с Cursor, Bolt и V0! Предоставьте более качественные подсказки для разработки интерфейса (создание навигационного веб-сайта с использованием искусственного интеллекта).
boy illustration
Новый бесплатный RooCline превосходит Cline v3.1? ! Быстрее, умнее и лучше вилка Cline! (Независимое программирование AI, порог 0)
boy illustration
Разработав более 10 проектов с помощью Cursor, я собрал 10 примеров и 60 подсказок.
boy illustration
Я потратил 72 часа на изучение курсорных агентов, и вот неоспоримые факты, которыми я должен поделиться!
boy illustration
Идеальная интеграция Cursor и DeepSeek API
boy illustration
DeepSeek V3 снижает затраты на обучение больших моделей
boy illustration
Артефакт, увеличивающий количество очков: на основе улучшения характеристик препятствия малым целям Yolov8 (SEAM, MultiSEAM).
boy illustration
DeepSeek V3 раскручивался уже три дня. Сегодня я попробовал самопровозглашенную модель «ChatGPT».
boy illustration
Open Devin — инженер-программист искусственного интеллекта с открытым исходным кодом, который меньше программирует и больше создает.
boy illustration
Эксклюзивное оригинальное улучшение YOLOv8: собственная разработка SPPF | SPPF сочетается с воспринимаемой большой сверткой ядра UniRepLK, а свертка с большим ядром + без расширения улучшает восприимчивое поле
boy illustration
Популярное и подробное объяснение DeepSeek-V3: от его появления до преимуществ и сравнения с GPT-4o.
boy illustration
9 основных словесных инструкций по доработке академических работ с помощью ChatGPT, эффективных и практичных, которые стоит собрать
boy illustration
Вызовите deepseek в vscode для реализации программирования с помощью искусственного интеллекта.
boy illustration
Познакомьтесь с принципами сверточных нейронных сетей (CNN) в одной статье (суперподробно)
boy illustration
50,3 тыс. звезд! Immich: автономное решение для резервного копирования фотографий и видео, которое экономит деньги и избавляет от беспокойства.
boy illustration
Cloud Native|Практика: установка Dashbaord для K8s, графика неплохая
boy illustration
Краткий обзор статьи — использование синтетических данных при обучении больших моделей и оптимизации производительности
boy illustration
MiniPerplx: новая поисковая система искусственного интеллекта с открытым исходным кодом, спонсируемая xAI и Vercel.
boy illustration
Конструкция сервиса Synology Drive сочетает проникновение в интрасеть и синхронизацию папок заметок Obsidian в облаке.
boy illustration
Центр конфигурации————Накос
boy illustration
Начинаем с нуля при разработке в облаке Copilot: начать разработку с минимальным использованием кода стало проще
boy illustration
[Серия Docker] Docker создает мультиплатформенные образы: практика архитектуры Arm64
boy illustration
Обновление новых возможностей coze | Я использовал coze для создания апплета помощника по исправлению домашних заданий по математике
boy illustration
Советы по развертыванию Nginx: практическое создание статических веб-сайтов на облачных серверах
boy illustration
Feiniu fnos использует Docker для развертывания личного блокнота Notepad
boy illustration
Сверточная нейронная сеть VGG реализует классификацию изображений Cifar10 — практический опыт Pytorch
boy illustration
Начало работы с EdgeonePages — новым недорогим решением для хостинга веб-сайтов
boy illustration
[Зона легкого облачного игрового сервера] Управление игровыми архивами
boy illustration
Развертывание SpringCloud-проекта на базе Docker и Docker-Compose