יום רביעי, 27 באוגוסט 2008

Garbage Collection in .NET - על שום מה ולמה?

פעמים רבות עולה בלבול מסוים שמדברים על עניין "איסוף הזבל" בשפות מבוסוסת NET.
מתכנתים שהגיעו מ C++ ודומיה פוחדים פחות מפצצה איראנית בדרך לארץ מאשר מזליגות זיכרון שונות משונות. הפחד התמידי של לשכוח משפט free קטן פה, לשחרר הקצאה שם... סיוט.
המתכנתים של מיקרוסופט החליטו לשאול מ- Java מאפיין נוסף - למה לסמוך על המתכנת שיוציא את הזבל שלו? בואו נשכור לו מנקה...
כך הגיע לעולם ה- Garbage Collector. המנגנון שאחראי לנקות את כל הזבל שהשארתם ב- Heap שלכם.
אז לפני ההסבר איך הוא עובד, הקדמה קצרה...

חלק א': מי ולמה?
כידוע, ישנם שני סוגי טיפוסים עיקריים בשפות מבוססות Net. - טיפוסים מבוססי ערך(Value Type) כגון int, struct ודומיהם וטיפוסים מבוססי יחס(Reference Type) כמו מחלקות למיניהן.
טיפוסים מבוססי ערך הם טיפוסים שקטים יחסית ולא בעייתיים, שכן הם מוקצים על המחסנית ומשתחררים אוטומטית בסוף חייה של הפונקציה - ולכן אין בעיות בשחרור הזיכרון שלהם.
עם אובייקטים סטטיים גם אין בעיה שכן הם נשמרים באזור הזיכרון הגלובלי של ה Process ומשתחררים כאשר התוכית הגיעה לקיצה.

הבעיה מגיעה בטיפוסי ה-Ref(טיפוסים מבוססי התייחסות). כאן נדרש מנגנון שישחרר את האובייקטים שאין בהם עוד שימוש.

חלק ב': תהליך ניקוי זבל
בתהליך ניקוי הזבל יש אלגוריתם שאחראי על ניקוי נכון ויעיל(עד כמה שאפשר).
תהליך הקריאה למנגנון ה- GC קורה בד"כ כאשר אנחנו מנסים לבצע פעולת new(הקצאה דינמית לצורך העניין) ואין מספיק זיכרון בשביל לעשות זאת. במצב כזה, מתבצע אלגוריתם ה-GC(שלבים ב' וד' יוסברו מיד):


א. הקפאת כל הת'ראדים הפועלים בתהליך הנוכחי(מלבד כמובן ת'ראדים שקשורים ל Unmanaged Code).
ב. בניית גרף האובייקטים.
ג. שחרור כל האובייקטים שלא נמצאים בגרף.
ד. דיחוס הזיכרון.
ה. עדכון כל ההצבעות(References) כך שיצביעו על האובייקטים בצורה נכונה.

בניית גרף האובייקטים
גרף האובייקטים הוא גרף שאחראי להכיל בתוכו את כל האובייקטים שאינם צריכים להתנקות.
הגרף נבנה באמצעות התייחסות ל Root References, כלומר: הגרף מסתכל על כל Root Reference ובונה מסלול של הצבעות לאוביקטים הקשורים אליו. האוביקטים האלו לא ישתחררו.
מהו Root Reference? כל האובייקטים הגלובלים/סטטיים בתכנית, כל אובייקט שמצביע על מחסנית של ת'ראד, כל אוגר שמכיל הפניה למקום כלשהו ב- Heap וכל אובייקט שהמצביע שלו נמצא ב F-reachable queue - אובייקטים שיש להם פונקציה הורסת.

דיחוס הזיכרון
התהליך מעביר את כל האובייקטים ש"שרדו" את הניפוי לתחתית הערימה, כך שיהיה מקום חדש ונקי בהתחלתה. הדבר קורה בצורה הבאה:
1. האלגוריתם סורק את הערימה בצורה ישרה ומחפשים חלקים סמוכים של אשפה(חלקים שאין בהם צורך עוד).
2. ה-GC מוריד את כל האובייקטים שאינם זבל לתחתית הרשימה, ומוריד את כל ה"רווחים" בערימה.
3. ה-GC מעדכן את ה References כך שיצביעו על האובייקטים במקומם החדש.
4. ה-GC מצביע כעת לאובייקט האחרון שאינו זבל, כך שידע שהמקום הבא בזכרון הוא מקום שיש בו זבל וניתן לשמור בו כל שרוצים.

חשוב לציין שקיימת ערימה נוספת - Large Managed Heap. בערימה זו מוקצים האובייקטים הגדולים(יותר מ-20K) והאובייקטים בערימה זו לא מועתקים למניעת רווחים בזיכרון, מכיוון שהעתקת אובייקטים גדולים כל כך תצרוך זמן יקר.

פונקציות של מחלקת Garbage Collection
במחלקה הדוטנטית של GC יש מספר פעולות סטטיות שמאפשרות שליטה קלה ב- GC.

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

GC.WaitForPendingFinalizers() - לאחר תהליך ה- GC הפנימי של NET. בת'ראד של ה-G, מתחילות לעבוד הפונקציות ההורסות בשאר הת'ראדים. פקודת ה GC.Collect() לא מחכה עד שפעולות אלו יסתיימו, ולכן נוצרה הפעולה הנ"ל. הפעולה הזו מחכה עד שכל הפונקציות ההורסות יסיימו פעולתן.

GC.SuppressFinalize(object obj) - הפונקציה הנ"ל אומרת למנגנון ה- GC שבאובייקט שמועבר אליו כפרמטר השתמשנו בפונקצית Dispose, ואין צורך להפעיל את הפונקציה הורסת עבור האוביקט הנ"ל.

GC.ReRegisterForFinalize(object obj) - פעולה הפוכה לפונקציה הקודמת. יש לרשום שוב את האובייקט כך שהפונקציה ההורסת שלו תופעל.

חלק ג': מסקנות
1. ההקצאה ב- NET. היא מהירה מאוד. בניגוד לב C++ שבכל הקצאה יש לחפש מקום פנוי על מנת להקצות בו זכרון, ב NET. ההקצאה תמיד מתבצעת בראש הערימה.
2. ניתן להבין כעת מדוע ב NET. אין אפשרות לקבל כתובת של אובייקט: אין לו כתובת קבועה! הכתובת של האובייקט משתנה כל הזמן על ידי ה-GC.
3. מנגנון ה- GC לא מתאים למערכות Realtime, שכן אסור שבזמן אמת בכל פעם שיופעל מנגנון ה-GC תהיה הקפאה של כל הת'ראדים, רגעית ככל שתהיה. יודגש, שלא ניתן למנוע GC אך ניתן לאלץ אותו לפעול.

בפוסט זה לא נכנסתי לעניין של F-reachable Queue מהסיבה שלרוב אין צורך להשתמש בדיסטרקטור(אלא אם יש עבודה ב Unmanaged Code, או כאשר רוצים לתת אבטחה נוספת לפונקצית Dispose).


יום שלישי, 26 באוגוסט 2008

אהלן.

אהלן וברוכים הבאים :)
מדי פעם אני אשתדל לפרסם כאן טיפים או דברים שנראים לי חשובים...

קורה הרבה שאני רוצה לכתוב על משהו בפורום כלשהו אבל אני יודע שתוך יומיים זה יעלם וחבל על ההשקעה.
אולי פה לא? :)