Net-Base Журнал

23.08.2026

Полювання на витоки пам'яті: цілеспрямоване використання FastMM FullDebugMode і коректне читання стек-трейсів

FastMM FullDebugMode є в Delphi-проектах одним із найпотужніших інструментів проти витоків пам'яті — але лише якщо його цілеспрямовано увімкнути, правильно інтерпретувати звіти та уникати типових хибних припущень. Цей практичний матеріал показує чіткий порядок дій від...

23.08.2026

Від теми журналу до практики проєкту

Відповідні сторінки послуг і технічні сторінки до публікації

Якщо Delphi-додаток у роботі поступово «роздувається», періодично завершується помилками Access Violations або після кількох днів роботи раптово стає нестабільним, за цим часто стоїть не одиничний баг, а шаблон: пам’ять виділяється, але не звільняється коректно — або звільняється занадто рано й потім ще використовується. Саме тут FastMM FullDebugMode виявляється неоціненним. Не як постійний режим, а як цілеспрямований діагностичний інструмент, який перетворює «десь у heap щось зламалося» на відтворювану причину.

Підступ у тому, що FullDebugMode генерує великий обсяг виводу, зменшує продуктивність і легко призводить до неправильних інтерпретацій. Звіт про витоки не обов’язково показує місце, де фактично «помилка» трапилася. А стек-трейс корисний лише настільки, наскільки коректно розв’язані символи (MAP-Datei, відладкова інформація, inlining). У цій статті я проходжу типовий прикордонний випадок, пояснюю чистий підхід і підводні камені — щоб наприкінці ти знаходив витоки й усував їх стійко.

Wann FastMM FullDebugMode wirklich sinnvoll ist

FastMM у сучасних версіях Delphi часто вже є дефолтним менеджером пам’яті або підключається у багатьох проєктах. FullDebugMode — це особлива конфігурація: він маркує блоки пам’яті додатковими контрольними патернами, збирає стек-трейси алокацій і агресивніше перевіряє на Heap-Korruption (тобто пошкоджені службові дані в heap, напр., через Buffer-Overruns).

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

  • Відтворюваний витік пам’яті: споживання пам’яті зростає під час тестового запуску на одну операцію (наприклад на кожен Request, під час імпорту, під час UI-дії).
  • Спорадичні AVs: особливо ті, що «іноді тут, іноді там» у тій самій ділянці (класичний випадок: Use-after-free).
  • Heap-Korruption: повідомлення на кшталт «Invalid pointer operation», «Access violation in ntdll» або аварії при завершенні/фіналізації.
  • Пошук регресій: після рефакторингу, оновлення бібліотеки або зміни компілятора раптово з’явилася нова нестабільність.

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

Grundprinzip: Leak-Report ist Symptom, Stacktrace ist Spur

Звіт про витоки спочатку показує: ці блоки на кінець роботи програми все ще алоковані. Це автоматично є проблемою лише тоді, коли ці блоки мали бути звільнені. Є легітимні «втрати»: глобальні Singletons, Caches, OS-Handles з життєвим циклом процесу або сторонні (third-party) бібліотеки, які навмисно не роблять фіналізації. Ці випадки потрібно знати, але не «ліпити патчі» навмання.

Стек-трейс у звіті показує місце, де блок був виділений. Часто це не те місце, де ти «забув викликати Free». Типова реальність у дорослій (історично розрослій) системі:

  • Алокування в UI- або сервісному шарі, звільнення має відбуватися в нижчому шарі (модель Ownership неясна).
  • Алокування у фабриці (Factory), Ownership передається виклику (Caller) — але Caller вважає, що воно «owned».
  • Об’єкти зберігаються в колекціях (Lists, Dictionaries), але модель Ownership неконсистентна.
  • Шлях винятку (Exception-Pfad) пропускає Cleanup, бо відсутній try/finally або він розташований занадто пізно.

Тому коректний порядок дій такий: відтворитиізолюватирозібрати Stacktraceзнайти помилку Ownershipвиправлення з регресійним тестом. FastMM надає сліди, але ти маєш інтерпретувати їх у контексті архітектури й життєвих циклів.

FastMM FullDebugMode sauber aktivieren (ohne Nebenwirkungen zu übersehen)

Schematische Grafik mit Heap-Blöcken und Prüfrändern, die in einen Leak-Report überführt werden
Абстракт: FullDebugMode працює з додатковими контрольними межами і виводом звіту.

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

Empfohlene Rahmenbedingungen für den Debug-Build

  • Debug DCUs та Debug-інформація: стек-трейси корисні лише якщо їх можна розв’язати до реальної одиниці/рядка/адреси. Переконайся, що генеруються відомості для відладки і що доступний MAP-файл.
  • Свідомо обирай оптимізації: для читабельності стек-трейсів зазвичай краще використовувати неоптимізовану збірку. Вбудовування (inlining) та агресивні оптимізації можуть «розмити» стек-фрейми.
  • Рівні умови виконання: використовуй по можливості ті самі дані, ту саму конфігурацію, ті самі права. Багато витоків залежать від даних (наприклад рідкісні формати, особливі шляхи).
  • Розділяй 64-bit і 32-bit: поведінка пам’яті, вирівнювання та сторонні бібліотеки відрізняються. Налагоджуй на цільовій платформі, де виникає проблема.

Один момент, який адміністратори та технічні лідери часто недооцінюють: FullDebugMode може також таймінг змінювати. Якщо задіяне багатопоточність, це може по-іншому впливати на прояви умов гонки. Тому має сенс паралельно мати запуск без FullDebugMode, який лише підтверджує відтворення. FullDebugMode тоді є кроком для діагностики.

Vorsicht bei „ReportMemoryLeaksOnShutdown“

Delphi може через ReportMemoryLeaksOnShutdown повідомляти про витоки при завершенні програми. Це зручно, але в складних застосунках (Services, Plug-in Host, lange Laufzeiten) може вводити в оману: під час завершення виконуються секції фіналізації, зупиняються потоки, звільняються кеші. Витік, який критичний у середині виконання, може зникнути до кінця — або навпаки: видимий витік виникає лише під час завершення, бо триває фонова робота.

Тому для практичної перевірки витоків важливіше: вимірювати витік на операцію (наприклад після 100 Requests), а не лише при завершенні. FastMM може в цьому допомогти, але тестова установка має це відтворювати.

Der typische Randfall: Leak-Report zeigt „irgendein Objekt“, aber die Ursache ist Ownership

Класика корпоративних застосунків: процес імпорту створює для кожного запису допоміжні об’єкти (наприклад, StringLists, JSON-Parser, тимчасові списки). В «щасливому» шляху вони коректно звільняються. У рідкісних випадках (пропуск через валідацію, Exception, ранній Exit) об’єкт залишається. Після 10 000 записів це стає помітним.

FastMM FullDebugMode тут допомагає, бо показує місце алокації. Але «фікс» не в тому, щоб просто free поставити в тому місці. Фікс — це надійний патерн власності (ownership):

  • Той, хто створює об’єкт, автоматично не є його власником.
  • Власність має бути чітко визначена в контракті API (параметри/повернення, документація, угоди щодо найменувань).
  • Колекції мають бути однозначними: володіючі (owning) та неволодіючі (non-owning). Змішані форми караються.
  • Шляхи з винятками потребують ранніх try/finally-блоків.

Якщо зі стек-трейсу ти бачиш лише „TStringList.Create“, інформація не марна — але вона каже тільки: тут щось створюється. Питання в іншому: де це має закінчитися? І тут архітектурне мислення допомагає більше, ніж акробатика відладника.

Стек-трейси правильно читати: що ти справді з них можеш вивести

Detailaufnahme einer Debugging-Analyse mit unscharfem Debugger und handnotierter Call-Chain
У стек-трейсі важливий ланцюжок викликів — не окремий рядок.

Стек-трейс від FastMM зазвичай — список адрес повернення, які — за наявності debug-символів — відображаються на Units, процедури і, у ідеалі, номери рядків. Коли ти це читаєш, вирішальними є три речі:

  • Вершина стеку не завжди відповідає за помилку: верхні фрейми часто належать менеджеру пам’яті/RTL. Цікаво стає там, де починається твій код.
  • Ланцюжок викликів замість одного рядка: рядок — це лише точка. Ланцюжок показує шлях, який привів до алокації.
  • Кілька ідентичних блоків: якщо FastMM повідомляє про кілька витоків однакового розміру, це часто означає повторюваний шлях. Це добре: у тебе є відтворюваність.

Якщо відсутні номери рядків: MAP-Datei, Packages, Release-DCUs

Багато команд спотикаються в цій точці: FullDebugMode увімкнено, звіт про витоки приходить, але замість Unit/Zeile є лише адреси або криптичні символи. Типові причини:

  • Відсутня MAP-Datei або не були згенеровані відладочні дані.
  • Ти працюєш з Release-DCUs або сторонніми DLL без символів.
  • Додаток використовує Runtime Packages: тоді частини коду лежать в BPLs, і розв’язування символів має цьому відповідати.
  • Оптимізація/Inlining зробили стек-трейс менш читабельним.

На практиці це означає: для полювання на витоки потрібен білд, який свідомо «пристосований до діагностики». Це інша мета, ніж «максимально швидко». Технічні Leads повинні розглядати це як окремий Build-Profil, щоб не кожен член команди ad hoc міняв опції проєкту.

Оцінка фреймів: „Цікаво“ часто — рядок вище

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

Правильне прочитання було б таким: Stacktrace показує тобі, де виникає блок. Фікс часто лежить у визначенні: Хто володіє об’єктом після Return? Якщо ти не відповіси на це питання чітко, ти лише змінюєш картину помилки (витік → AV).

Пошкодження купи vs. витік пам’яті: чому FullDebugMode часто знаходить справжнього винуватця

Графіка, що показує переповнення буфера, яке заходить у сусідню ділянку пам'яті
Пошкодження купи часто проявляється з затримкою — FullDebugMode робить його видимим раніше.

Багато «витоків» насправді є наслідковими проблемами: переповнення буфера перезаписує метадані купи, менеджер пам’яті потім не може коректно звільнити пам’ять, і в підсумку ти бачиш нібито випадкові витоки або операції з недійсними вказівниками. FullDebugMode корисний тут, бо працює з контрольними зразками й при Free/Reuse виконує додаткові валідації.

Важливо зробити розрізнення:

  • Витік пам’яті: блок було алоковано і ніколи не звільнено. Стабільність страждає з часом, крах не є обов’язковим.
  • Використання після звільнення: блок звільнений, але пізніше ще використовується. Призводить до спорадичних AVs, які важко відтворити.
  • Подвійне звільнення: блок звільняється двічі. Може впасти одразу або пізніше (коли блок було повторно використано).
  • Пошкодження купи: хтось записує за межі блоку. Симптоми часто з’являються з затримкою.

FullDebugMode особливо цінний, коли ти бачиш симптоми з відтермінуванням. Додаткова валідація робить помилки видимими раніше — часто саме в місці, де відбувається некоректний доступ, а не хвилинами пізніше в будь-якому виклику Free.

Підхід у проєктах: відтворюване полювання на витоки замість «відлагодження в тумані»

Якщо ти хочеш полювати на витоки пам’яті, тобі потрібен процес, який можна відтворити і передати в командах. Я люблю працювати з фіксованою діагностичною рамкою:

1) Відтворення в детермінованому сценарії

Визнач тестову послідовність, яка надійно демонструє витік: „Стартуй Service, оброби 500 повідомлень, зупини Service“ або „Відкрий форму X, виконай дію Y 200 разів“. Важливо документувати послідовність з параметрами (заповнення даних, тенант, Feature-Flags), щоб інші могли її повторити.

2) Мінімізувати: зробити витік видимим по кроках

Якщо послідовність займає 20 хвилин, розбий її. Мета: ти хочеш якомога швидше порівнювати «до» і «після». У великих застосунках це часто і є основним витратником часу, а не саме виправлення.

3) FullDebugMode einschalten und Report interpretieren

Jetzt erst kommt FastMM FullDebugMode ins Spiel. Sammle die Reports, gruppiere nach Blockgröße/Callstack und schaue nach Wiederholungen. Ein einzelner verbleibender Block kann ein legitimer Cache sein. 10.000 identische Blöcke sind fast immer ein echter Leak.

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

Fixe Leaks dort, wo Ownership definiert wird: Factory, API-Vertrag, Collection-Wrapper. „Schnell ein Free einbauen“ direkt neben Create ist oft der falsche Ort, wenn das Objekt weitergereicht wird.

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

Der Fix ist erst dann gut, wenn die Sequenz wieder läuft und weder Leaks noch neue Speicherfehler auftreten. Gerade bei Use-after-free ist ein „Leak weg“ kein Beweis, sondern nur ein neues Symptom.

Typische Fallstricke in Delphi-Code, die FastMM sichtbar macht

Collections und Ownership (Listen, Dictionaries, Interfaces)

Viele Leaks kommen nicht aus komplizierten Algorithmen, sondern aus alltäglichen Datenstrukturen. Zwei klassische Fehlerbilder:

  • Eine Liste enthält Objekte, aber niemand weiß, wer sie freigibt. Lösung: owning Liste verwenden oder konsequent im finally räumen.
  • Ein Dictionary hält Objekte als Values; beim Remove wird der Value nicht freigegeben oder beim Clear vergessen.

Zusätzlich tricky sind Interfaces: Referenzzählung (ARC-ähnlich) ist komfortabel, aber Mischbetrieb mit Objekt-Ownership kann bei zyklischen Referenzen oder Events Leaks erzeugen. FullDebugMode zeigt dir dann oft den Allokationspfad, aber die Ursache ist ein Referenzzyklus (A hält B über Interface, B hält A über Callback).

Exceptions und frühe Exits

In gewachsenen Business-Software-Systemen sind Exceptions oft Teil der normalen Steuerung (z. B. Validierung, Abbruch, Retry). Das Problem ist selten die Exception selbst, sondern der Pfad drumherum: Ein Objekt wird vor dem try/finally erzeugt, dann fliegt eine Exception, und der Cleanup wird übersprungen. FullDebugMode liefert dir den Stacktrace der Allokation – und du musst prüfen, ob es einen garantiert ausgeführten Freigabepfad gibt.

Threads und Lebenszeit: „Freigeben im falschen Thread“

Bei VCL/FMX und Services mit Worker-Threads entsteht ein weiterer Randfall: Ein Objekt wird in einem Thread erzeugt, aber im UI-Thread freigegeben (oder umgekehrt), weil man per Queue/Synchronize „nur schnell“ etwas rüberreicht. Das kann funktionieren, kann aber auch zu Use-after-free führen, wenn der Producer weiterarbeitet, während der Consumer bereits freigibt.

FastMM FullDebugMode kann hier helfen, weil er zeitversetzte Fehler früher detektiert. Der eigentliche Fix ist jedoch ein sauberes Lebenszeitmodell: klare Besitzverhältnisse, Übergabe nur über immutable Daten oder eindeutige Ownership-Transfer-Punkte.

Wie du Reports nutzbar machst: Filtern, vergleichen, dokumentieren

In Teams lohnt es sich, Leak-Reports nicht nur „anzuschauen“, sondern wie ein Artefakt zu behandeln. Drei pragmatische Maßnahmen, die sich bewährt haben:

  • Baseline-Report: Ein „bekannter Zustand“ (z. B. aktuelle Produktversion) wird einmal mit FullDebugMode durchlaufen und als Referenz abgelegt. Dann erkennst du neue Leaks sofort.
  • Vergleich nach Use-Case: Für kritische Workflows (Import, Export, API-Request, UI-Massenoperation) legst du jeweils eine kurze Sequenz fest, die regelmäßig wiederholbar ist.
  • Dokumentierte „legitime Leaks“: Wenn ein Cache bewusst nicht finalisiert, dokumentiere das. Sonst jagt in sechs Monaten jemand wieder die gleichen Einträge.

Це не бюрократія, а економія часу: полювання за витоками пам’яті інакше швидко перетворюється на безкінечний цикл, оскільки ті самі шаблони повторюються в кожному спринті.

Коли зусилля виправдані — і коли варто діяти інакше

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

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

Якщо ж у тебе невеликий десктопний хелпер, який завершується через 30 секунд, виявлення витоків часто відходить на другий план. Так само: якщо це одноразова проблема пікового використання пам’яті (наприклад, великий експорт), це часто не витік, а питання стратегії потокової обробки та пікових навантажень у купі.

Практичний висновок: FullDebugMode — це не перемикач, а процес

FastMM FullDebugMode надає структуру пошуку помилок пам’яті: він робить виділення пам’яті видимими, виявляє пошкодження купи на ранній стадії та надає стек-трейси, за якими можна виправляти причину, а не симптом. Вирішальний важіль — не сам інструмент, а процес: відтворювані сценарії, збірки, придатні для діагностики, чіткі правила володіння ресурсами та регресійне тестування щодо базової версії.

Якщо ти застряг на впертій витоку або спорадичній помилці купи й хочеш забезпечити тривалу стабільність цієї проблеми в більшому Delphi-системі, варто зробити коротке, акуратно організоване діагностичне налаштування з чіткою послідовністю та звітами, придатними для аналізу. Якщо потрібна підтримка з аналізу, профілів збірки або рефакторингу архітектури — зв’яжіться з Net-Base Software GmbH.

Для цієї теми також важливі Delphi Виявлення витоків пам’яті та Читання Fastmm Leak Report. Стаття систематизує ці аспекти зрозуміло і показує, на що звертати увагу в щоденній практиці.

Обговорити проєкт або план модернізації з Net-Base.

Наступний крок

Якщо тема перетворюється на реальний проєкт, архітектуру, наявні системи та експлуатацію слід розглядати разом на ранньому етапі.

Ми підтримуємо не лише в окремих питаннях, а й тоді, коли з уривків вихідного коду, питань, пов’язаних із legacy, або ідей порталу має вирости надійний корпоративний проєкт.

  • Поточний стан, цільова архітектура та технічні ризики оцінюються спільно.
  • REST, доступ до даних, портали та Rollout не відсуваються на пізніший етап.
  • Ви заздалегідь бачите, який шлях є економічно та операційно життєздатним.

Поділитися дописом

Поділитися цим дописом безпосередньо

LinkedIn, X, XING, Facebook, WhatsApp та E‑Mail доступні негайно. Для Instagram ми безпосередньо готуємо посилання та короткий текст.

Електронна пошта

Instagram відкривається в новій вкладці. Посилання та короткий текст попередньо копіюються у буфер обміну.