Net-Base מגזין

23.08.2026

ציד דליפות זיכרון: שימוש ממוקד ב־FastMM FullDebugMode וקריאה נכונה של Stacktraces

FastMM FullDebugMode הוא אחד הכלים היעילים ביותר נגד דליפות זיכרון בפרויקטים של Delphi — אך רק אם מפעילים אותו במכוון, מפרשים נכון את הדוחות וממנעים הנחות שגויות טיפוסיות. מאמר מעשי זה מציג מהלך עבודה מסודר מ...

23.08.2026

מהנושא במגזין ליישום בפרויקט

דפי שירות וטכניים רלוונטיים למאמר

אם יישום Delphi-יישום במהלך ההפעלה מתחיל „להתנפח“ לאט, קורס מדי פעם עם Access Violations או אחרי ימים של ריצה הופך לפתע לא יציב, מאחורי זה לרוב לא עומד באג יחיד אלא דפוס: זיכרון מבוקש אך לא משוחרר כראוי — או משוחרר מוקדם מדי ונעשה בו שימוש מאוחר יותר. בדיוק כאן FastMM FullDebugMode שווה זהב. לא כמצב קבוע, אלא ככלי אבחון ממוקד שממזר את ה״משהו בהריפ תקול״ חזרה לסיבה שניתן לעקוב אחריה.

הבעיה: FullDebugMode מייצר הרבה פלט, צורכת ביצועים ומובילה במהירות לפרשנויות שגויות. דוח דליפה לא בהכרח מראה אוטומטית את המקום שבו „הבאג“ נמצא. ו‑Stacktrace טוב רק כמה שטובת היא פתרון הסמלים (MAP‑קובץ, מידע דיבאג, inlining). בפוסט הזה אני עובר את המקרה השולי הטיפוסי, מסביר את הגישה הנקייה ואת המוקשים — כך שבסוף לא רק תאתר דליפות אלא גם תתקן אותן ביסודיות.

מתי FastMM FullDebugMode אכן הגיוני

בגרסאות מודרניות של Delphi FastMM לעיתים קרובות כבר מנהל הזיכרון המוגדר כבררת מחדל או משולב בפרויקטים רבים. עם זאת, FullDebugMode היא תצורה מיוחדת: היא מסמנת בלוקי זיכרון בתבניות בדיקה נוספות, אוספת עקבות סטאק של הקצאות ובודקת בצורה אגרסיבית יותר שחיתות Heap (כלומר נתוני ניהול פגומים ב‑Heap, למשל כתוצאה מ‑Buffer‑Overruns).

אני מפעיל את FullDebugMode במכוון כאשר מתקיים אחד מהמקרים הבאים:

  • דליפה שניתנת לשחזור: צריכת הזיכרון עולה במהלך ריצת בדיקה לפי פעולה (למשל per בקשה, per ייבוא, per פעולה בממשק משתמש).
  • AVs ספורים: במיוחד כאלה שמתרחשים „לפעמים כאן, לפעמים שם“ באותו אזור (קלסיקה: Use‑after‑free).
  • שחיתות Heap: הודעות כמו „Invalid pointer operation“, „Access violation in ntdll“ או קריסות בזמן סיום/Finalize.
  • חיפוש רגרסיות: אחרי refactoring, עדכון ספריה או שינוי קומפיילר מופיעה פתאום חוסר יציבות חדש.

לא נכון להפעיל FullDebugMode כ“נשנה את זה בכל ה‑builds“. ההעמסה גבוהה, ה‑timing משתנה, והוא עלול לגרום ש‑Race‑Conditions ייעלמו או יזוזו. לתפעול שוטף מתאים יותר מוניטורינג קל (למשל Working Set של התהליך, Private Bytes, מונים לכל פעולה) — FullDebugMode הוא הסקלפל, לא מד דופק.

עיקרון בסיסי: דוח דליפה הוא סימפטום, Stacktrace הוא רמז

דוח דליפה מציג בפניך קודם כל: הבלוקים האלה נשארים ממוּקצים בסיום התוכנית. זה בעיה רק אם בלוקים אלה היו אמורים להישחרר בפועל. קיימות דליפות „לגיטימיות“: סינגלטונים גלובליים, מטמונים (Caches), OS‑Handles עם זמן‑חיים של התהליך או ספריות צד שלישי שלא מבצעות שחרור מכוון. את המקרים האלה חשוב לדעת, אבל לא לתקן אותם בעיוורון.

ה‑Stacktrace בדוח מציג את המקום שבו הבלוק הוקצה. זה לעיתים אינו המקום שבו „שכחת להקרא Free“. מציאות נפוצה במערכות שצמחו לאורך זמן:

  • הקצאה בשכבת ה‑UI או בשכבת השירות, והשחרור אמור להתרחש בשכבה עמוקה יותר (בעלות/Ownership לא ברורה).
  • הקצאה בפקטורי, ה‑Ownership מועבר לקרלר — אבל הקולר מניח שהוא הבעלים (thinks it is „owned“).
  • אובייקטים מאוחסנים בקולקשנים (רשימות, Dictionaries), אך מודל ה‑Ownership אינו עקבי.
  • נתיב חריגות מדלג על ניקוי כי try/finally חסר או מתחיל מאוחר מדי.

הזרימה הנקייה היא לכן: לשחזרלבודדלפענח את ה-Stacktraceלמצוא שגיאות Ownershipתיקון עם בדיקת רגרסיה. FastMM מספקת לך את העקבות, אבל עליך לתרגם אותן לארכיטקטורה ולמחזורי החיים.

הפעלת FastMM FullDebugMode באופן נקי (בלי לפספס השפעות לוואי)

תרשים סכמטי עם בלוקים של heap ושולי בדיקה שמומר לדוח דליפות
תקציר: FullDebugMode עובד עם שולי בדיקה נוספים ופלט דוחות.

הפעלת FullDebugMode נעשית בפועל דרך אפשרויות FastMM וקובץ קונפיגורציה מתאים של FastMM. הקריטי הוא פחות „איך בדיוק קוראים לקובץ ה-include“, ויותר מה הקונפיגורציה גורמת ו-בתנאי בנייה אילו אתה משתמש בה.

תנאי מסגרת מומלצים ל-Debug-Build

  • Debug DCUs und Debug-Infos: Stacktraces שימושיים רק אם ניתן לפתור אותם ל-Unit/שורה/כתובת אמיתית. ודא שמתבצעת יצירת מידע דיבאג ושקובץ MAP זמין.
  • בחירת אופטימיזציה במודע: עבור קריאות Stacktrace בנייה ללא אופטימיזציות בדרך כלל עדיפה. Inlining ואופטימיזציות אגרסיביות עלולות „למטשטש“ מסגרות סטאק.
  • תנאי ריצה זהים: השתמש ככל האפשר באותם נתונים, אותה קונפיגורציה, אותם הרשאות. רבות מהדליפות תלויות בנתונים (למשל פורמטים נדירים, מסלולים מיוחדים).
  • להפריד 64-bit vs. 32-bit: התנהגות הזיכרון, ה-alignment וספריות צד שלישי שונים. בצע דיבאג בפלטפורמת היעד שבה הבעיה מתרחשת.

נקודה שמנהלי מערכת ומקבלי החלטות טכניות לעיתים מזלזלים בה: FullDebugMode יכול גם לשנות את ה-תזמון (Timing). אם יש מעורבות של ריבוי חוטים (Threading), תנאי מירוץ עלולים להופיע בצורה שונה. לכן כדאי להחזיק במקביל גם ריצה ללא FullDebugMode שמאמתת רק את השחזור. FullDebugMode הוא אז הצעד לאבחון.

זהירות לגבי „ReportMemoryLeaksOnShutdown“

Delphi יכול לדווח על דליפות בהשבתת התוכנית דרך ReportMemoryLeaksOnShutdown. זה נוח, אבל ביישומים מורכבים (שירותים, מארחי תוספים, ריצות ארוכות) זה עלול להטעות: בזמן השבתה פועלים מקטעי Finalization, תהליכים נעצרים ונקיים מטמונים. דליפה שהיא קריטית באמצע הריצה עלולה להיעלם עד הסוף — או להפך: דליפה לכאורה נוצרת רק בשבתה כי עדיין יש עבודה ברקע.

לצורך צידת דליפות מעשית חשוב יותר: למדוד דליפות לפרוצדורה (למשל אחרי 100 Requests), ולא רק בעת סגירה. FastMM יכול לסייע בכך, אבל תצורת המבחן חייבת לשקף זאת.

המקרה השולי הטיפוסי: דוח דליפות מציג „אובייקט כלשהו“, אבל הסיבה היא Ownership

מוכר בתוכנות ארגוניות: תהליך ייבוא יוצר עבור כל רשומה אובייקטים עזר (למשל StringLists, JSON-Parser, רשימות זמניות). במסלול התקין הם משוחררים כראוי. במקרים נדירים (דלג בגלל ולידציה, Exception, יציאה מוקדמת) אובייקט נשאר תקוע. לאחר 10.000 רשומות זה ניכר.

FastMM FullDebugMode עוזר כאן, כי הוא מציג את מקום ההקצאה. אבל ה„תיקון“ אינו „free במקום ההקצאה“. התיקון הוא דפוס בעלות אמין:

  • מי שיוצר אובייקט אינו בעלים באופן אוטומטי.
  • בעלות חייבת להיות מוגדרת באופן ברור בחוזה ה-API (פרמטר/החזרה, תיעוד, קונבנציות שמות).
  • אוספים חייבים להיות ברורים: בעלים (owning) לעומת לא-בעלים (non-owning). צורות מעורבות נגמלות.
  • נתיבי Exceptions צריכים בלוקי try/finally מוקדמים.

אם מה-Stacktrace אתה רואה רק „TStringList.Create“, המידע אינו חסר ערך – אבל הוא רק אומר: כאן משהו נוצר. השאלה היא: היכן זה אמור להסתיים? וכאן חשיבה ארכיטקטונית עוזרת יותר מאקרובטיקה של הדיבאגר.

קריאת Stacktraces נכונה: מה אפשר באמת להסיק מזה

תקריב של ניתוח דיבאגינג עם דיבאגר מטושטש ושרשרת קריאות כתובה ידנית
ב-Stacktrace מה שחשוב הוא שרשרת הקריאות (Call-Chain) – לא השורה הבודדת.

Stacktrace מ-FastMM הוא בדרך כלל רשימת כתובות חזרה, אשר – עם סמלי דיבאג – ממופות ל-Units, פרוצדורות ובמקרה האידיאלי למספרי שורות. כשאתה קורא את זה, שלוש נקודות הן מכריעות:

  • Top-of-Stack אינו תמיד השגיאה: ה-frames העליונים לעתים קרובות הם של Memory-Manager/RTL. המקום המעניין הוא שם שבו מתחיל הקוד שלך.
  • Call-Chain במקום שורה בודדת: השורה היא רק נקודה. השרשרת מראה לך איזה מסלול הוביל להקצאה.
  • בלוקים זהים מרובים: אם FastMM מדווח על מספר דליפות זיכרון בגודל זהה, זה לעיתים קרובות מסלול חוזר. זה טוב: יש לך יכולת לשחזר.

כשחסרים מספרי שורות: קובץ MAP, Packages, Release-DCUs

צוותים רבים נתקעים כאן: FullDebugMode פעיל, דוח הדליפות מופיע, אבל במקום Unit/שורה יש רק כתובות או סמלים מסתוריים. סיבות טיפוסיות:

  • אין קובץ MAP או שפרטי הדיבאג לא נוצרו.
  • אתה רץ כנגד Release-DCUs או DLLs של צד שלישי ללא סמלים.
  • היישום משתמש ב-Runtime Packages: אז חלקי הקוד נמצאים ב-BPLs, ופתרון הסמלים חייב להתאים לכך.
  • אופטימיזציה/Inlining הפכו את ה-Stacktrace לפחות קריא.

בעשייה מעשית זה אומר: לצידוד דליפות אתה זקוק לבילד שמכוון במודע ל“יכולת אבחון“. זו מטרה שונה מ“מקסימום מהירות“. מובילי טכנולוגיה צריכים לטפל בזה כפרופיל בילד נפרד, כדי שלא כל חבר צוות ישנה אופציות פרויקט באופן אד-הוק.

הערכת פריימים: „מעניין“ הוא לעתים קרובות שורה אחת למעלה

דוגמה מהמציאות (ללא קוד לקוח קונקרטי): Der Stacktrace מציג בפעם הראשונה בקוד שלך את הרוטינה „LoadConfig“. אתה רואה שם יצירת אובייקט. אתה מוסיף קריאת Free, דליפת הזיכרון נעלמת — ופתאום זה קורא במקום אחר עם Double Free. למה? כי „LoadConfig“ שמים את האובייקט בקאש, ונתיב קוד אחר כבר הוא ה-Owner ומנקה מאוחר יותר.

הקריאה הנכונה היתה: ה-Stacktrace מראה לך, איפה הבלוק נוצר. התיקון נמצא לעיתים קרובות בהגדרה: מי הוא הבעלים של האובייקט אחרי ה-Return? אם אתה לא עונה על שאלה זו בצורה ברורה, אתה רק משנה את תמונת השגיאה (דליפת זיכרון → AV).

שחיתות Heap לעומת דליפת זיכרון: למה FullDebugMode לעתים קרובות מוצא את העבריין האמיתי

Grafik, die einen Buffer-Overrun zeigt, der in benachbarten Speicherbereich überläuft
שחיתות ה-heap מתבטאת לעיתים קרובות בהשהיה – FullDebugMode עושה אותה גלויה מוקדם יותר.

רבות מה“דליפות“ הן בפועל בעיות משניות: Buffer-Overrun כותב על מטא-נתוני ה-heap, מנהל הזיכרון לא מצליח לשחרר כראוי מאוחר יותר, ובסוף אתה רואה דליפות לכאורה אקראיות או פעולות עם מצביעים לא חוקיים. FullDebugMode חזק בהקשר הזה כי הוא עובד עם דפוסי בדיקה ומבצע ולידציות נוספות ב-Free/Reuse.

חשובה ההבחנה:

  • דליפת זיכרון: הבלוק הוקצה ולא שוחרר מעולם. היציבות נפגעת לאורך זמן, קריסה אינה בהכרח מיידית.
  • Use-after-free: הבלוק שוחרר, אך משמש מאוחר יותר. מוביל ל-AVים אקראיים שקשה לשחזר.
  • שחרור כפול: הבלוק משוחרר פעמיים. עלול לגרום לקריסה מיידית או רק מאוחר יותר (כאשר הבלוק שוחזר/הוקצה מחדש).
  • שחיתות של ה-heap: מישהו כותב מעבר לגבולות של בלוק. התסמינים מופיעים לעיתים קרובות בהשהיה.

FullDebugMode יקר ערך במיוחד כשאתה רואה תסמינים בהשהיה. הוולידציה הנוספת הופכת שגיאות לנראות מוקדם יותר – לעיתים קרובות בדיוק במקום שבו מתבצעת הגישה השגויה, ולא רק דקות אחר כך בקריאת Free אקראית.

נהלים בפרויקטים: מרדוף דליפות שניתן לשחזר במקום „דיבוג בערפל“

אם אתה רוצה לצוד דליפות זיכרון, אתה צריך תהליך שניתן לחזור עליו ולשתף בצוותים. אני עובד בדרך כלל עם מסגרת אבחון קבועה:

1) שיחזור בתרחיש דטרמיניסטי

הגדר רצף בדיקה שמראה את הדליפה בצורה אמינה: „Starte Service, verarbeite 500 Nachrichten, stoppe Service“ או „Öffne Maske X, führe Aktion Y 200-mal aus“. חשוב לתעד את הרצף עם פרמטרים (מאגר נתונים, לקוח, Feature-Flags), כך שאחרים יוכלו לשחזר אותו.

2) מיזעור: להציג את הדליפה בכל שלב

אם הרצף נמשך 20 דקות, חלק אותו. המטרה היא: אתה רוצה להשוות במהירות בין „לפני“ ו“אחרי“. במערכות גדולות זה לעיתים קרובות החלק שגוזל זמן רב, לא התיקון עצמו.

3) להפעיל את FullDebugMode ולפרש את הדוח

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 הוא כלי אבחון הכרוך בעלויות. המאמץ משתלם במיוחד כאשר:

  • היישום פועל לאורך זמן (שירות, לקוח Terminalserver, מערכת משמרות, תהליכים 24/7).
  • אתה מעבד זרימות נתונים של לקוחות אמיתיים ולא מכסה את כל הנתיבים במבחנים.
  • היציבות חשובה יותר מהמהירות של פיתוח תכונות לטווח הקצר (טיפוסי בפתרונות תוכנה הקרובים לתהליך).

אם מצד שני יש לך רק כלי שולחני קטן שמסתיים אחרי 30 שניות, מרדף הדליפות לעתים אינו בעדיפות עליונה. באותו אופן: אם יש לך בעיית Memory-Spike חד-פעמית (למשל ייצוא גדול), בדרך כלל זו אינה דליפה אלא שאלה של אסטרטגיית סטרימינג ועומס שיא ב‑heap.

מסקנה מעשית: FullDebugMode אינו מתג אלא תהליך

FastMM FullDebugMode מטיל סדר בחיפוש שגיאות בזיכרון: הוא מציג הקצאות, מגלה קריסות ב‑heap מוקדם יותר ומספק Stacktraces שבעזרתן תוכל לתקן את הסיבה ולא רק את התסמין. המנוף המכריע אינו הכלי עצמו אלא ההליך: תרחישים שניתנים לשחזור, builds המתאימים לאבחון, הסכמי בעלות ברורים ורגרסיה מול Baseline.

אם אתה תקוע עם דליפה עקשנית או שגיאת heap ספורדית וברצונך לייצב את הנושא בצורה בת-קיימא בתוך Delphi-מערכת, משתלם לבצע התקנת אבחון קצרה ונקייה עם רצף ברור ודוחות הניתנים לניתוח. אם אתה צריך תמיכה בניתוח, בפרופילי build או ב‑refactoring ארכיטקטוני: פנה אל Net-Base Software GmbH.

לנושא זה חשובות גם Delphi מציאת דליפות בזיכרון וקריאת Fastmm Leak Report. המאמר ממקם היבטים אלה בצורה מובנת ומראה מה חשוב בשגרה.

לדון בפרויקט או ביוזמת מודרניזציה עם Net-Base.

השלב הבא

כאשר נושא זה הופך לפרויקט אמיתי, יש לבחון כבר בשלבים המוקדמים את הארכיטקטורה, המערכות הקיימות והתפעול יחד.

אנו תומכים לא רק בשאלות נקודתיות, אלא גם כשמקטעי קוד מקור, נושאי Legacy או רעיונות פורטל אמורים להפוך לפרויקט ארגוני מהימן ועמיד.

  • המצב הקיים, תמונת היעד והסיכונים הטכניים מוערכים יחד.
  • REST, גישה לנתונים, פורטלים ופריסה לא יידחו כהשלכות מאוחרות.
  • מבעוד מועד אתם רואים איזה נתיב בר-קיימא מבחינה כלכלית ותפעולית.

שתף פוסט

לשתף את הפוסט הזה ישירות

LinkedIn, X, XING, Facebook, WhatsApp ודוא"ל זמינים מיידית. ל‑Instagram אנו מכינים קישור וטקסט קצר ישירות.

דוא״ל

אינסטגרם נפתח בכרטיסייה חדשה. הקישור וטקסט קצר מועתקים מראש ללוח.