В предыдущей статье я рассказывал о сборщике мусора Java. 

Примечание переводчика; На Хабре есть несколько статей о сборке мусора Java, например: Избавляемся от мусора в Java или Разбираемся со сборкой мусора в Java
Поэтому, видимо, нет необходимости переводить первую статью автора.

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

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

Каковы последствия утечек памяти?

К сожалению, это часто означает, что утечки памяти могут перенести в производственную среду, что вызовет проблемы у конечных пользователей. Например, вот эта недавняя история об утечках памяти, тормозящих последние компьютеры Apple M1 . Виртуальная память позволяет операционным системам эффективно справляться с утечками памяти в течение очень долгого времени. Однако потеря производительности может стать заметной.

Со сборщиком мусора мы часто полагаемся на автоматическое управление памятью для решения таких проблем с памятью. Но все не так просто в реальной жизни. Как правило, эту проблему сложнее решить для языков с ручным управлением памятью и учетом ссылок. Я специально исключаю Rust из этого обсуждения. У меня недостаточно опыта с ним, чтобы сказать что-то ценное. Это кажется очень интересным, но имеет некоторые сложности .

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

Типы динамической памяти

Одной из проблем с отслеживанием памяти кучи является управление ожиданиями. Вы ожидаете, что выделение памяти вызовет эквивалентный рост памяти, а освобождение памяти восстановит ситуацию. Это не всегда так.

Большинство утечек памяти происходит в куче, но бывают редкие случаи, когда источник утечки может находиться в нативном коде, пространстве PermGen и т. д. Мы должны отлаживать нативные проблемы, используя собственные инструменты для работы с памятью. Мы можем настроить другие типы памяти с помощью флагов JVM. Вы часто можете определить источник утечки памяти, просматривая сообщение об ошибке нехватки памяти. Распространены следующие типы:

  • PermGen space (пространство PermGen) — это было распространено в старых JVM, особенно с инструментами, которые выполняют тяжелые манипуляции с байт-кодом. Сегодня это не так распространено благодаря динамическому пространству PermGen.

  • Java heap space/Requested array size exceeds VM limit/Out of swap space? (пространство кучи Java/запрошенный размер массива превышает лимит виртуальной машины/недостаточно места в файле подкачки?) и т. д. — это, вероятно, означает, что утечка находится в вашем коде или в сторонней библиотеке. Но эта проблема в Java коде, что является хорошей новостью!

  • Если стек указывает на нативный метод — это может быть связано с утечкой нативного метода.

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

Ваш Tool Box

Существует МНОГИЕ инструменты профилирования для отслеживания/фиксации утечек памяти. Невозможно дать полный обзор даже небольшому сегменту доступного богатства. Я не буду вдаваться даже в часть того, что доступно. Вместо этого я сосредоточусь на двух инструментах: VisualVM и Chrome DevTools (с упором на Node).

VisualVM позволяет нам просмотреть работающее приложение, чтобы получить моментальный снимок использования памяти. Chrome DevTools — это отладчик более общего назначения, который включает в себя инструменты для JavaScript разработчиков. Он может подключаться к работающему node приложению и отлаживать его. Я не буду обсуждать:

  • Java Flight Recorder (JFR) и Mission Control — эти инструменты фактически заменяют VisualVM. Но они не так удобны. Да, они могут обнаруживать частую сборку мусора и т. д., но они не идеальны для мелкозернистой отладки. Бортовой самописец также имеет проблемы с лицензированием. Если вы хотите использовать это вместо этого, ознакомьтесь с этой статьей Ашиша Чоудхари. (На Хабре: Управление Java Flight Recorder и О Java Mission Control

  • Yourkit Profiler, Eclipse MAT, NetBeans Profiler, Parasoft Insure++ и т. д. — все это отличные инструменты, которые могут значительно помочь в более глубоком копании, но они требуют обзора продукта, а не технической статьи.

  • LeakCanary — есть и другие мобильные инструменты, но опять же, я хочу больше сосредоточиться на общем бэкенде.

  • Valgrind — это интересный родной инструмент для отладки утечек памяти в Linux.

  • Библиотека CRT — для визуальной студии Microsoft предоставляет несколько отличных примитивов.

  • Некоторые инструменты статического анализа, такие как SonarCloud или FindBugs, могут обнаруживать утечки. Это не обнаружит все утечки, но может указать на некоторые проблемные случаи.

VisualVM

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

На скриншоте выше VisualVM отслеживает себя, что довольно просто. Вы можете выполнить ручную сборку мусора, что очень важно для понимания размера утечки. График кучи дает общее представление об объеме памяти с течением времени и тенденциях.

Инструменты разработчика Chrome

Если вы тестировали интерфейсы в Chrome, вы наверняка сталкивались с инструментами отладки, которые интегрированы в Chrome. Лично я предпочитаю эквиваленты Firefox. Они могут довольно легко подключаться к Node, где они могут предоставлять многие стандартные возможности отладки, такие как моментальные снимки.

Как обнаружить утечки?

Утечки довольно очевидны, когда вы видите, как память увеличивается, и вы не видите, как она уменьшается. Но как определить источник утечки?

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

void leakUnitTest() {
    performRiskyOperation();
    System.gc();
    Thread.sleep(1000);
    Runtime r = Runtime.getRuntime();
    long free = r.freeMemory();
    for(int iter = 0 ; iter < 100 ; iter++) {
        performRiskyOperation();
    }
    System.gc();
    Thread.sleep(1000);
    assertThat(Math.abs(r.freeMemory() - free) < validThreshold);
}

Здесь происходит много вещей, поэтому давайте рассмотрим их по отдельности:

  • Я запускаю рискованную операцию один раз перед запуском — это важно. Статический код и инициализация переменных занимают оперативную память, но это не является утечкой

  • Я явно запускаю System.gc(). Это возможно не на всех языках и обычно не рекомендуется. Но это "работает"

  • Даже явный сборщик мусора может иметь асинхронные элементы, поэтому задержка - это нормально.

  • Я запускаю тест 100 раз, чтобы убедиться, что небольшая утечка не накапливается.

  • У меня есть порог допустимых значений. Сборщики мусора не идеальны. Мы должны принять, что сбор некоторых элементов может занять некоторое время. Java API имеет много встроенного статического контекста (например, пулы в примитивных объектах), что может привести к незначительному неизбежному увеличению объема памяти. Однако это число не должно быть слишком большим.

Еще одно важное замечание — использовать простой сборщик мусора при запуске этого теста (в целом хорошая практика). Рекомендую прочитать мой предыдущий пост на эту тему.

Проблема в пороге. Это фактически сводит на нет многие преимущества теста, но, к сожалению, от этого никуда не деться.

Давайте рассмотрим менее «автоматизированный» способ обнаружения утечек. В идеале, это то, что платформы будут готовы решать в будущем.

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

Оперативная память периодически увеличивается?

Что, если оперативная память просто съедается, пока вы буквально ничего не делаете?

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

Сравните снимки, чтобы найти тип объекта

Самый важный инструмент в нашем арсенале — дамп кучи. В VisualVM вы можете получить дамп, нажав кнопку в правом верхнем углу. Он выглядит так:

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

С помощью Chrome DevTools вы можете сделать снимок, используя основной пользовательский интерфейс:

Затем вы можете использовать просмотр, сортировку и фильтрацию результирующих объектов в моментальных снимках:

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

Вы также можете использовать подробный GC (трассировка GC в NodeJS), чтобы просмотреть подробную информацию о собранном объекте. Мне часто кажется, что это немного похоже на питье из пожарного шланга. С таким объемом вывода очень сложно отлаживать даже простое приложение. Но это может быть полезно, если вы ищете что-то очень конкретное.

Распространенные типы утечек памяти

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

Кроме того, статический контекст — это всегда риск, поэтому вам нужно остерегаться этого и пытаться свести его к минимуму. Обратите внимание, что singleton по-прежнему является статическим контекстом…

Strings

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

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

Скрытая семантика

Хорошим примером этого является код Swing, подобный этому:

new JTable(myModel);

Разработчики часто удаляют объект JTable и сохраняют модель. Но из-за того, как MVC работает в некоторых средах пользовательского интерфейса (таких как Swing, Codename One и т. д.), представление регистрируется как листнер модели. Это означает, что, если вы сохраните ссылку на модель, JTable нельзя будет удалить.

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

Решение для этого простое: используйте отладчики!

Не только для отладки кода. Но для проверки сторонних объектов. Вам необходимо ознакомиться с объектами, которые хранятся в составе этих библиотек.

Утечка контекста

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

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

session.store(myUserData);

Но если myUserData включает ссылку на глобальные данные или данные других пользователей, мы можем утечь данные этих пользователей при каждом новом сеансе.

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

Утечка ресурсов

При проведении исследования для этой статьи обнаружилось, что почти в каждом посте упоминалась утечка файловых ресурсов и т. д. Это отдельная проблема. Утечки файловых ресурсов были проблемой 20 лет назад для некоторых ОС. Текущий сборщик мусора и очистка делают так, что эти утечки почти не имеют значения.

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

Как мы можем предотвратить утечки?

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

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

Пишите безопасный код при создании сложных API. IntelliJ/IDEA имеет довольно комплексный код для привязки элементов IDE к плагинам. Это идеальное место для утечек и багов. Поэтому умные разработчики из JetBrains добавили в свой код журналы, которые обнаруживают такие утечки при выгрузке. Возьмите страницу из их книги, предскажите будущие проблемы… Если у вас есть API, который позволяет разработчикам регистрироваться, подумайте о способе обнаружения утечек. Распечатайте список оставшихся объектов до того, как приложение будет разрушено. Возможно это из-за утечки памяти!

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

Наконец, запустите монитор памяти в своем приложении. Просмотрите объекты, имеют ли они смысл?

Попробуйте объяснить логику объектов, которые вы видите в оперативной памяти. Например, если в вашем приложении много объектов byte[], но не используются отображения или примитивные данные, может возникнуть утечка.

TL;DR

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

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

В первую очередь:

  • Создавайте модульные тесты на утечку памяти, хотя они и ненадежны

  • Запустите тесты на виртуальной машине с ограниченным объемом оперативной памяти.

  • Напишите API, которые регистрируют оставшиеся связанные объекты при выходе

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

  • Периодически проверяйте использование оперативной памяти вашими приложениями и пытайтесь понять объекты, которые вы видите перед собой.

Спасибо, что дочитали до этого места. Следите за мной в твиттере, чтобы узнать больше.

Комментарии (2)


  1. vladd12
    28.01.2022 12:21
    +5

    Какой-то немного машинный перевод... Или мне кажется?


    1. Vest
      28.01.2022 12:45
      +1

      Я бы сказал, что и статья слабая.