Статьи

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

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

Давайте начнем с описания различия в управлении памятью в Java и, например, в языках Си. Когда C-программист хочет использовать переменную, он должен вручную выделить область в памяти, где будет находиться значение. После того, как приложение завершит использование этого значения, область памяти должна быть освобождена вручную, то есть код, освобождающий память, должен быть написан разработчиком. В Java, когда разработчик хочет создать и использовать новый объект с использованием, например, нового Integer (5) , ему не нужно выделять память — об этом позаботится виртуальная машина Java (JVM). В течение срока службы приложения JVM периодически проверяет, какие объекты в памяти все еще используются, а какие нет. Неиспользуемые объекты могут быть сброшены, а память восстановлена ​​и снова использована. Этот процесс называетсясборка мусора и соответствующая часть JVM называется сборщиком мусора , или GC.

Автоматическое управление памятью в Java опирается на GC, который периодически ищет неиспользуемые объекты и удаляет их. И тут прячется дракон. Если немного упростить, можно сказать, что утечка памяти в Java — это ситуация, когда некоторые объекты больше не используются приложением, но GC не распознает их как неиспользуемые . В результате эти объекты остаются в памяти на неопределенное время, уменьшая объем памяти, доступной для приложения.

Здесь я хотел бы подчеркнуть один очень важный момент: понятие «объект больше не используется приложением» полностью, абсолютно, на 100% специфично для приложения ! Помимо некоторых конкретных случаев, когда продолжительность жизни объекта может быть определена логически (например, локальная переменная метода, которая ни при каких обстоятельствах не исключает метод), использование объекта может быть понято только разработчиком приложения с учетом всего использования шаблоны приложения.

Как GC может отличить неиспользуемые объекты от тех, которые приложение будет использовать в определенный момент времени в будущем? Основной алгоритм можно описать следующим образом:

  1. Есть некоторые объекты, которые GC считают «важными». Они называются корнями GC и (почти) никогда не выбрасываются. Они, например, в настоящее время выполняют локальные переменные метода и входные параметры, потоки приложения, ссылки из собственного кода и подобные «глобальные» объекты.
  2. Предполагается, что любой объект, на который ссылаются эти корни GC, используется и не удаляется. Один объект может ссылаться на другой по-разному в Java, чаще всего, когда объект A хранится в поле объекта B. В этом случае мы говорим «B ссылается на A».
  3. Вышеупомянутый процесс повторяется до тех пор, пока все возражаемые, которые могут быть транзитивно достигнуты от корней GC, не посещены и не помечены как «используемые».
  4. Все остальное не используется и может быть выброшено.

Теперь довольно просто создать Java-программу, которая удовлетворяет приведенному выше определению утечки памяти:

public class Calc {
  private Map cache = new HashMap();
  public int square(int i) {
    int result = i * i;
    cache.put(i, result);
    return result;
  }
  public static void main(String[] args) throws Exception {
    Calc calc = new Calc();
    while (true) {
      System.out.println("Enter a number between 1 and 100");
      int i = readUserInput(); //not shown
      System.out.println("Answer " + calc.square(i));
    }
  }
}

 

Эта программа читает одно число за раз от своего пользователя и вычисляет его квадратное значение. Эта реализация использует примитивный «кэш» для хранения результатов вычислений. Но так как эти результаты никогда не считываются из кеша, блок кода представляет утечку памяти в соответствии с нашим определением выше. Если мы позволим этой программе работать и взаимодействовать с пользователями достаточно долго, «кэшированные» результаты будут занимать много памяти.

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

Что приводит нас к простому следствию: утечка памяти похожа на хорошее вино — она ​​нуждается в старении ? Если вы хотите продемонстрировать утечку или, что более важно, исправить ее, вы действительно должны позволить ей расти. Крошечные утечки памяти теряются во всех тех объектах, которые присутствуют в приложении в любой данный момент времени. Независимо от того, какой инструмент вы используете для выявления утечек памяти — будь то профилировщик, анализатор дампов памяти, APM или специальный инструмент для поиска утечек, такой как Plumbr — должно быть много объектов, которые изжили себя. Это означает, что ваше приложение должно работать в течение значительного периода времени И как можно больше разных частей вашего приложения. В противном случае вы будете искать иголку в стоге сена.

Если вы хотите узнать больше об утечках памяти Java, особенно о различных способах их обнаружения и исправления в ваших приложениях, ознакомьтесь с нашей серией постов в блоге под названием «Решение проблемы с ошибками» . Чтобы узнать больше об инструменте, который мы создаем для решения этой проблемы, обратитесь к нашему короткому экрану . И следите за обновлениями в нашем твиттере @JavaPlumbr . До следующего раза!