איסוף זבל (מדעי המחשב)

איסוף זבל (באנגלית: Garbage collection) הוא תהליך שבו סביבת הריצה של תוכנית מחשב משחררת באופן אוטומטי זיכרון שהוקצה דינאמית ואין בו עוד צורך.
המנגנון הומצא ויושם לראשונה בשנת 1959 על ידי ג'ון מקארתי עבור שפת Lisp. איסוף זבל נפוץ בשפות תכנות מונחות-עצמים מודרניות כגון Java ו-#C‏, וכן בשפות המורצות על ידי מפרש כמו Perl, ‏פייתון, ‏PHP ו-JavaScript. יש שפות כגון עדה ושפת D (ובמידה מסוימת גם Delphi) המאפשרות למתכנת לבחור אם להשתמש במנגנון זה או לנהל הקצאות ושחרור זיכרון באופן עצמאי.
בהקשר הנוכחי, "זבל" הוא כל מקטע זיכרון שהוקצה באופן דינמי ואין בו ולא יהיה בו שימוש נוסף בתוכנית. מצב בו זיכרון "זבל" לא משוחרר מכונה "דליפת זיכרון" (Memory leak) ועלול להביא למצב בו משאבי הזיכרון שהתוכנית משתמשת בהם הולכים וגדלים ובסופו של דבר אף לקריסה (הפסקת ריצה) של התוכנית.
איסוף זבל חוסך מהמתכנת את המעקב אחר זיכרון שהוקצה במהלך ריצת התוכנית ואת שחרורו באופן מפורש. באופן עקרוני, יש קושי עצום להגדיר שפת תכנות המכילה רפרנסים (או מצביעים) כך שתהיה בעלת טיפוסיות חזקה ובטוחה, אם היא הכוללת שחרור זיכרון מפורש (במקום איסוף זבל).
חסרונו הבולט של המנגנון הוא תוספת מסוימת של תקורות בביצועי התוכנית בפועל, ובמיוחד התנהגות לא דטרמיניסטית של זמן הריצה באופן שאיננו ברור מקוד התוכנית, היבט משמעותי במיוחד עבור תוכניות זמן אמת. בנוסף, מנגנוני איסוף זבל לא מזהים דליפת זיכרון שנובעת מהחזקה של אובייקטים שאינם בשימוש בתוך מבני נתונים (כגון רשימה) שנמצאים בשימוש, ואינם יכולים למנוע דליפת זיכרון מסוג זה.

יישום איסוף אוטומטי בשפות תכנות

רוב שפות התכנות המודרניות כגון Java‏, Basica‏, Lisp, פייתון, C#‏ ו- Ruby (כחלק מכלל פלטפורמת ה-‎.NET) משחררות את המתכנת מניהול מפורש של שחרור הזיכרון שהוקצה, ומותירה את הניהול למנגנון איסוף זבל שמתבצע בזמן ריצה.
שפת C לא מגדירה איסוף זבל, ועל המתכנת מוטלת האחריות לשחרר באופן מפורש כל זיכרון שהוקצה. עם זאת, השפה לא אוכפת בזמן קומפילציה את ניהול הזיכרון. מסיבה זו, דליפות זיכרון הן בעיה חמורה ושכיחה ביותר בתכנות בשפת C, וישנן תוכנות מיוחדות שמטרתן לעזור לאתר דליפות זיכרון.
בשפת ++C, בדומה לשפת C, לא קיים איסוף זבל אוטומטי, אבל ניתן לדמות איסוף זבל אוטומטי באמצעות "מצביעים חכמים", אובייקטים שעוטפים מצביעים רגילים ומנהלים את איסוף הזבל של הזיכרון אליו הם מצביעים באופן אוטומטי (למשל באמצעות מניית התייחסויות). מנגנון זה פותר חלק מהבעיות הקשורות בדליפת זיכרון, אך לא מאפשר לטפל באופן אוטומטי בכל תבנית של ניהול זיכרון.
שפת Rust אוכפת בזמן קומפילציה, דרך מערכת הטיפוסים, ניהול חוקי של הזיכרון. בכך הופכת השפה את איסוף הזבל בזמן ריצה לטריוויאלי, ומשיגה יעילות יחד עם היעדר דליפות זיכרון. המחיר הוא מערכת טיפוסים מורכבת ולעיתים לא אינטואיטיבית, שדורשת השקעה משמעותית על מנת לקמפל תוכניות.

טכניקות איסוף זבל אוטומטי

קיימות שתי טכניקות עיקריות של איסוף זבל אוטומטי.

איסוף זבל ב-‎.NET וב-Java

כאמור לעיל, מספר שפות משתמשות במנגנון איסוף זבל אוטומטי. המכונה הווירטואלית מחייבת שכל האובייקטים יוקצו בערימה המנוהלת (managed heap). המתכנת לא משחרר אובייקטים מהערימה, אלא הם משוחררים אוטומטית כאשר לאפליקציה אין עוד שימוש בהם. האלגוריתם שבו מתבצע איסוף הזבל ב-‎.NET הוא סימון ומחיקה.

Finalization

אוסף הזבל מתמודד בהצלחה עם מעקב אחר אובייקטים שהאפליקציה יוצרת לצורך שחרורם, אך נכשל בהתמודדות עם משאבים בלתי מנוהלים, כגון חלון, חיבור לקובץ או לרשת. את המשאבים הללו יש לשחרר ידנית בסיום השימוש בהם. בפלטפורמת .NET ובשפת Java קיימת השיטה Object.Finalize, שיטה שאוסף הזבל מריץ על האובייקט כדי לנקות את המשאבים הבלתי מנוהלים של אותו אובייקט, לפני שהוא משחרר את הזיכרון של האובייקט. מאחר שהשיטה הבסיסית אינה עושה דבר, על המתכנת לדרוס אותה (override) אם יש צורך בשחרור משאבים פרטני. להבדיל מ-destructor, שיטת ה-Finalize נקראת כאשר אוסף הזבל מגיע לניקוי האובייקט, בעוד ה-destructor מתבצע מיידית כאשר האובייקט יוצא משימוש.
שיטה זו מעכבת את עבודתו של אוסף הזבל האוטומטי, שכן הוא יצטרך לעבור על האובייקט בשני "סיבובים" לפני שהאובייקט ישוחרר לחלוטין. כאשר אובייקט חדש, לו יש שיטת Finalize, מוקצה בערימה, מתווסף מצביע לאובייקט ב-Finalization queue (תור ה-Finalization). כאשר יש לשחרר את האובייקט, אוסף הזבל יחפש בתור מצביע לאותו אובייקט, ולאחר שיימצא, יעביר את המצביע לאובייקט אל תור אחר - Freachable queue - וטכנית האובייקט לא ייחשב זבל. בסיום שלב המחיקה (sweep), אוסף הזבל מריץ את שיטת ה-Finalize של כל אובייקט בתור ה-Freachable. בסיבוב הבא של אוסף הזבל, האובייקטים שהופעלה שיטת ה-Finalize שלהם יזוהו כזבל אמיתי, ולבסוף ישוחרר הזיכרון שהוקצה להם. לכן, מומלץ להשתמש בשיטת ה-Finalize רק בעת צורך, משום שהם גורמים לאובייקטים להשתחרר מהזיכרון מאוחר יותר, ובכך לבזבוז זיכרון: נדרשים שני סיבובים של אוסף הזבל על מנת לפנותם.

Weak Reference

מצביע חלש (Weak Reference) עוזר להוריד לחץ של אובייקטים גדולים על הערימה המנוהלת. כפי שצוין, אובייקט שיש לו מצביע רגיל (הנקרא מצביע חזק) לא ישוחרר על ידי אוסף הזבל, מכיוון שלאפליקציה יש גישה אליו. אך כאשר לאובייקט יש רק מצביעים חלשים, אוסף הזבל כן ישחרר את הזיכרון, אף על פי שעדיין קיים מצביע לאזור ההוא בזיכרון. לכן, כאשר התוכנה תנסה לגשת לאזור ההוא בעזרת אותם מצביעים חלשים, הניסיון ייכשל. אם קיימים מצביעים חזקים וחלשים לאובייקט מסוים הוא לא ישוחרר. אחד השימושים למצביעים חלשים הוא כאשר יש רצון לחסוך בזיכרון, בדרך כלל כשהתוכנה מתעסקת באובייקטים שתופסים מקום גדול בזיכרון, לדוגמה, מיפוי של דיסק קשיח בעזרת עץ - לכל אחד מהצמתים בעץ יהיה מצביע חלש. לפני גישה לכל אחד מהם תהיה בדיקה האם הוא שוחרר על ידי אוסף הזבל או לא, ובמידה שכן, הצומת ההוא בלבד ימופה. ההשלכה לדרך עבודה זו היא אמנם הורדת יעילות זמן הריצה של התוכנה, אך הקטנת הנפח שיתפוס בזיכרון.