Net-Base Журнал

23.08.2026

Поиск утечек памяти: целенаправленное использование FastMM FullDebugMode и корректное чтение стек-трейсов

FastMM FullDebugMode в Delphi-проектах является одним из самых серьёзных инструментов против утечек памяти — но только если его целенаправленно включать, правильно интерпретировать отчёты и избегать типичных ошибочных предположений. В этом практическом материале показан корректный порядок действий от...

23.08.2026

От темы в журнале к проектной практике

Соответствующие страницы услуг и технологий к статье

Когда Delphi-приложение в эксплуатации постепенно «раздувается», периодически выходит с Access Violations или после нескольких дней работы внезапно становится нестабильным, за этим часто стоит не отдельный баг, а закономерность: память выделяется, но не освобождается корректно — или освобождается слишком рано и затем снова используется. Именно в таких случаях FastMM FullDebugMode оказывается крайне полезен. Не как постоянный режим, а как прицельный инструмент диагностики, который из «где-то в куче что-то сломано» делает воспроизводимую причину.

Загвоздка в том, что FullDebugMode генерирует много вывода, стоит производительности и легко приводит к неверной интерпретации данных. Отчёт об утечке не показывает автоматически место, где «ошибка» находится. А стек-трейс хорош ровно настолько, насколько хороша разрешающая символы информация (MAP-файл, отладочные сведения, inlining). В этой статье я разбираю типичный пограничный случай, поясняю корректный подход и подводные камни — чтобы в конце вы находили утечки не только оперируя симптомами, но устраняли их надёжно.

Когда FastMM FullDebugMode действительно имеет смысл

FastMM в современных версиях Delphi часто уже является менеджером памяти по умолчанию или подключается во многих проектах. FullDebugMode — это особая конфигурация: он помечает блоки памяти дополнительными контрольными шаблонами, собирает allocation-stacktraces и агрессивнее проверяет целостность кучи (то есть повреждённые управляющие данные в куче, например buffer-overruns).

Я использую FullDebugMode целенаправленно, когда присутствует один из следующих сценариев:

  • Воспроизводимая утечка: использование памяти растёт в тестовом прогоне на операцию (например, на запрос, на импорт, на действие в UI).
  • Эпизодические AV: особенно такие, которые «то тут, то там» происходят в одной и той же области (классический: use-after-free).
  • Повреждение кучи: сообщения наподобие «Invalid pointer operation», «Access violation in ntdll» или аварии при завершении/финализации.
  • Поиск регрессии: после рефакторинга, обновления библиотеки или смены компилятора возникла новая нестабильность.

FullDebugMode не имеет смысла включать «во всех сборках по умолчанию». Накладные расходы велики, тайминги меняются, и именно race-condition могут из‑за этого исчезнуть или сместиться. Для постоянной эксплуатации больше подходит лёгкий мониторинг (например, Working-Set процесса, Private Bytes, счётчики на операцию) — FullDebugMode это скальпель, а не пульсометр.

Основной принцип: отчёт об утечке — симптом, стек-трейс — след

Отчёт об утечке показывает прежде всего: эти блоки к концу работы программы всё ещё выделены. Это автоматически становится проблемой только если эти блоки должны были быть освобождены. Существуют легитимные «утечки»: глобальные singletons, кэши, дескрипторы ОС с временем жизни процесса или сторонние библиотеки, которые намеренно не финализируются. Эти случаи важно знать, но не следует слепо «закрывать».

Стек-трейс в отчёте указывает место, где блок был запрошен. Это часто не то место, где вы забыли вызвать Free. Типичные реальные сценарии в развитых системах:

  • Выделение в UI‑ или сервисном слое, освобождение должно происходить в более низком слое (владение неясно).
  • Выделение в фабрике, владение передаётся вызывающему коду (caller) — но вызывающий думает, что объект у него «во владении».
  • Объекты хранятся в коллекциях (списки, словари), но модель владения непоследовательна.
  • Путь через исключение пропускает очистку, потому что try/finally отсутствует или начинается слишком поздно.

Таким образом корректный порядок действий: воспроизвестиизолироватьразобрать стек вызововнайти ошибку владения (ownership)исправление с регрессионным тестом. FastMM предоставляет следы, но тебе нужно перевести их в понятия архитектуры и жизненных циклов.

Корректная активация FastMM FullDebugMode (чтобы не пропустить побочные эффекты)

Schematische Grafik mit Heap-Blöcken und Prüfrändern, die in einen Leak-Report überführt werden
Аннотация: FullDebugMode работает с дополнительными проверочными границами и генерацией отчёта.

FullDebugMode в практике включается через опции FastMM и соответствующую конфигурацию FastMM. Существенно не столько «как именно называется файл include», сколько что делает конфигурация и при каких условиях сборки ты её используешь.

Рекомендуемые условия для Debug‑сборки

  • Debug DCUs и отладочная информация: стек-трейсы полезны только если их можно разрешить до реальной Unit/Zeile/Adresse. Убедись, что генерируются отладочные данные и доступен MAP‑файл.
  • Осознанный выбор оптимизации: для читаемости стек-трейса обычно лучше не оптимизированная сборка. Инлайнинг и агрессивные оптимизации могут «размывать» фреймы стека.
  • Идентичные условия выполнения: используй по возможности те же данные, ту же конфигурацию, те же права. Многие утечки зависят от данных (например, редкие форматы, особые ветви исполнения).
  • Разделять 64-bit и 32-bit: поведение памяти, выравнивание и сторонние библиотеки различаются. Отлаживай на целевой платформе, где проявляется проблема.

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

Осторожно с „ReportMemoryLeaksOnShutdown“

Delphi может сообщать об утечках при завершении программы через ReportMemoryLeaksOnShutdown. Это удобно, но в сложных приложениях (сервисы, хост плагинов, долгие времена работы) может вводить в заблуждение: при shutdown выполняются секции finalization, останавливаются потоки, очищаются кэши. Утечка, критичная в середине выполнения, может исчезнуть к концу — или наоборот: кажущаяся утечка возникает только при завершении, потому что ещё идёт фоновая работа.

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

Типичный граничный случай: отчёт об утечке показывает „irgendein Objekt“, но причина — владение (ownership)

Классика корпоративных приложений: процесс импорта для каждой записи создаёт вспомогательные объекты (например, StringLists, JSON-парсеры, временные списки). В «happy path» они корректно освобождаются. В редких случаях (пропуск по валидации, исключение, преждевременный выход) объект остаётся. После 10.000 записей это становится заметно.

FastMM FullDebugMode помогает здесь, потому что показывает место аллокации. Но «фикс» — это не «free в месте аллокации». Реальное решение — это надёжный паттерн владения:

  • Тот, кто создаёт объект, не автоматически является его владельцем.
  • Владение должно быть явно зафиксировано в контракте API (параметры/возврат, документация, соглашения по именованию).
  • Коллекции должны быть однозначными: owning vs. non-owning. Смешанные формы приводят к проблемам.
  • Пути с исключениями требуют ранних try/finally-блоков.

Если в стек-трейсе вы видите только «TStringList.Create», эта информация не бесполезна — но она говорит лишь: здесь что-то создаётся. Вопрос: где это должно завершиться? И в этом случае архитектурное мышление помогает больше, чем акробатика отладчика.

Правильное чтение стек-трейсов: что вы действительно можете из них извлечь

Детальный снимок анализа отладки с размытым отладчиком и вручную помеченной цепочкой вызовов
В стек-трейсе важна цепочка вызовов — не отдельная строка.

Стек-трейс из FastMM обычно представляет собой список адресов возврата, которые — при наличии отладочных символов — сопоставляются с Units, процедурами и, в идеале, номерами строк. При его чтении решающими являются три момента:

  • Верх стека не всегда указывает на ошибку: верхние фреймы часто — менеджер памяти/RTL. Интересно становится там, где начинается ваш код.
  • Цепочка вызовов вместо одной строки: строка — это лишь точка. Цепочка показывает, какой путь привёл к аллокации.
  • Несколько одинаковых блоков: если FastMM сообщает о нескольких утечках одинакового размера, это часто указывает на повторяющийся путь. Это хорошо: у вас есть воспроизводимость.

Если отсутствуют номера строк: MAP-файл, пакеты, Release-DCUs

Многие команды спотыкаются на этом: FullDebugMode включён, отчёт об утечках готов, но вместо Unit/строки видны только адреса или криптические символы. Типичные причины:

  • Нет MAP-файла или отладочная информация не была сгенерирована.
  • Вы используете Release-DCUs или сторонние DLL без символов.
  • Приложение использует Runtime-пакеты: тогда части кода находятся в BPLs, и разрешение символов должно соответствовать этому.
  • Оптимизация/инлайнинг сделали стек-трейс менее читабельным.

На практике это означает: для охоты за утечками нужен билд, сознательно настроенный на «диагностичность». Это другая цель, чем «максимальная скорость». Технические лиды должны рассматривать это как отдельный профиль сборки, чтобы не приходилось каждому участнику команды ad hoc менять опции проекта.

Оценка фреймов: «интересным» часто оказывается фрейм на одну строку выше

Пример из реальной практики (без конкретного клиентского кода): Stacktrace показывает вам в первом фрейме вашего кода процедуру „LoadConfig“. Вы видите там создание объекта. Вы добавляете вызов Free, утечка исчезает — и внезапно в другом месте происходит авария с Double Free. Почему? Потому что „LoadConfig“ помещает объект в кеш, и другой путь выполнения уже является владельцем и позже освобождает его.

Правильная интерпретация была бы такой: Stacktrace показывает вам, где возникает блок. Исправление чаще всего находится в определении: кто владеет объектом после возврата? Если вы не дадите на этот вопрос чёткого ответа, вы лишь измените картину ошибки (Leak → AV).

Повреждение кучи vs. Leak: почему FullDebugMode часто находит истинного виновника

Grafik, die einen Buffer-Overrun zeigt, der in benachbarten Speicherbereich überläuft
Повреждение кучи часто проявляется с задержкой — FullDebugMode выявляет это раньше.

Многие «Leak» на самом деле являются вторичными проблемами: переполнение буфера перезаписывает метаданные кучи, менеджер памяти позже не может корректно освободить блок, и в итоге вы видите кажущиеся случайными утечки или операции с недействительными указателями. FullDebugMode в таких случаях эффективен, потому что он использует проверочные шаблоны и выполняет дополнительные валидации при Free/Reuse.

Важно провести различие:

  • Leak: блок был аллоцирован и никогда не освобождён. Со временем страдает стабильность, крах не обязателен.
  • Use-after-free: блок освобождён, но позже всё ещё используется. Приводит к спорадическим AV, которые трудно воспроизвести.
  • Double Free: блок освобождается дважды. Может вызвать немедленный крах или проявиться позже (если блок был повторно использован).
  • Heap-Korruption: кто-то записывает за границы блока. Симптомы часто проявляются с задержкой.

FullDebugMode особенно ценен, когда вы видите симптомы с задержкой. Дополнительные проверки делают ошибки видимыми раньше — часто именно в том месте, где происходит неверный доступ, а не через несколько минут в каком‑то произвольном Free.

Подход в проектах: воспроизводимая охота на утечки вместо «отладки в тумане»

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

1) Воспроизведение в детерминированном сценарии

Определите тестовую последовательность, которая надёжно демонстрирует утечку: «Запустите сервис, обработайте 500 сообщений, остановите сервис» или «Откройте форму X, выполните действие Y 200 раз». Важно задокументировать последовательность с параметрами (набор данных, тенант, Feature-Flags), чтобы другие могли её воспроизвести.

2) Минимизация: сделать утечку видимой на каждом шаге

Если последовательность занимает 20 минут, разделите её. Цель: вы хотите как можно быстрее сравнить «до» и «после». В крупных приложениях это часто основной потребитель времени, а не само исправление.

3) Включить FullDebugMode и интерпретировать отчёт

Только теперь вступает в игру FastMM FullDebugMode. Соберите отчеты, сгруппируйте по размеру блока/стеку вызовов и ищите повторяющиеся записи. Одиночный оставшийся блок может быть легитимным кэшем. 10 000 идентичных блоков почти всегда означают реальную утечку.

4) Ownership-Klärung und Fix in der passenden Schicht

Исправляйте утечки там, где определяется Ownership: Factory, API-Vertrag, Collection-Wrapper. «Быстро вставить Free» прямо рядом с Create часто не то место, если объект передаётся дальше.

5) Regression: gleiche Sequenz, gleicher Build, gleicher Report

Исправление считается удачным только тогда, когда последовательность выполняется снова и ни утечек, ни новых ошибок с памятью не возникает. Особенно при use-after-free отсутствие «утечки» не является доказательством, а лишь новым симптомом.

Typische Fallstricke in Delphi-Code, die FastMM sichtbar macht

Collections und Ownership (Listen, Dictionaries, Interfaces)

Многие утечки возникают не из-за сложных алгоритмов, а из-за повседневных структур данных. Два классических сценария ошибок:

  • Список содержит объекты, но никто не знает, кто их освобождает. Решение: использовать owning‑Liste или последовательно очищать в finally.
  • Словарь держит объекты в качестве Values; при Remove значение не освобождается или забывается при Clear.

Дополнительно проблемными являются Interfaces: подсчёт ссылок (похожий на ARC) удобен, но смешанный режим с объектной Ownership может приводить к утечкам при циклических ссылках или событиях. FullDebugMode часто покажет путь аллокации, но причиной будет цикл ссылок (A держит B через Interface, B держит A через Callback).

Exceptions und frühe Exits

В развивающихся бизнес‑приложениях исключения часто являются частью нормального управления (например валидация, прерывание, повторная попытка). Проблема редко в самой исключении, а в пути вокруг неё: объект создаётся до try/finally, затем выбрасывается исключение и очистка пропускается. FullDebugMode предоставляет стек вызовов аллокации — и вам нужно проверить, существует ли гарантированный путь освобождения.

Threads und Lebenszeit: „Freigeben im falschen Thread“

В VCL/FMX и службах с Worker‑Threads возникает ещё один крайний случай: объект создаётся в одном потоке, но освобождается в UI‑потоке (или наоборот), потому что «быстро» передали его через Queue/Synchronize. Это может работать, но может привести к use-after-free, если производитель продолжает работать, пока потребитель уже освобождает объект.

FastMM FullDebugMode может помочь здесь, потому что он раньше детектирует временно смещённые ошибки. Однако реальное исправление — это чистая модель времени жизни: чёткие Besitzverhältnisse, передача только через immutable‑данные или однозначные Ownership‑Transfer‑точки.

Wie du Reports nutzbar machst: Filtern, vergleichen, dokumentieren

В командах стоит не просто «просматривать» Leak‑отчёты, а обращаться с ними как с артефактом. Три прагматичных меры, которые зарекомендовали себя:

  • Baseline-Report: «известное состояние» (например текущая версия продукта) один раз прогоняется с FullDebugMode и сохраняется как эталон. Тогда новые утечки видно сразу.
  • Vergleich nach Use-Case: для критичных рабочих процессов (Import, Export, API-Request, UI‑Massenoperation) задайте короткую повторяемую последовательность для регулярного прогона.
  • Dokumentierte „legitime Leaks“: если кэш сознательно не финализируется, задокументируйте это. Иначе через шесть месяцев кто‑то снова полезет за теми же записями.

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

Когда затраты оправданы — и когда стоит поступить иначе

FastMM FullDebugMode — диагностический инструмент с сопутствующими издержками. Затраты оправданы особенно в случаях, когда:

  • Приложение работает длительное время (служба, клиент терминального сервера, система сменной работы, процессы 24/7).
  • Обрабатываются реальные потоки данных клиентов и не все пути покрыты тестами.
  • Стабильность важнее краткосрочной скорости добавления функциональности (типично для прикладного, процессно-ориентированного ПО).

Если же у вас лишь небольшой десктопный хелпер, который завершает работу через 30 секунд, поиск утечек часто вторичен. Аналогично: при единичном всплеске потребления памяти (например, крупный экспорт) это часто не утечка, а вопрос стратегии стриминга и пиковой нагрузки в куче.

Практический вывод: FullDebugMode — это не переключатель, а процесс

FastMM FullDebugMode придаёт структуру поиску ошибок памяти: он делает видимыми выделения памяти, раньше выявляет повреждения кучи и предоставляет трассировки стека, с помощью которых можно исправлять причину, а не симптом. Решающий рычаг при этом — не инструмент, а порядок действий: воспроизводимые сценарии, билды, пригодные для диагностики, чёткие соглашения об ответственности/владении (Ownership) и регресс-тестирование относительно эталонной базы.

Если вы застряли на упорной утечке памяти или спорадической ошибке кучи и хотите устойчиво стабилизировать эту проблему в более крупной системе Delphi, имеет смысл провести короткую, аккуратную настройку диагностики с чёткой последовательностью действий и отчётами, пригодными для анализа. Если нужна поддержка по анализу, профилям сборок или рефакторингу архитектуры: обратитесь к Net-Base Software GmbH.

Для этой темы также важны Delphi Поиск утечки памяти и Fastmm Leak Report Lesen. Статья систематизирует эти аспекты в понятной форме и показывает, на что обращать внимание в повседневной работе.

Обсудить проект или задачу по модернизации с Net-Base.

Следующий шаг

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

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

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

Поделиться записью

Поделиться этой записью напрямую

LinkedIn, X, XING, Facebook, WhatsApp и электронная почта доступны немедленно. Для Instagram мы готовим ссылку и краткий текст.

Электронная почта

Instagram открывается в новой вкладке. Ссылка и короткий текст предварительно копируются в буфер обмена.