Garbage Collector (GC) ב-Java
בניגוד לשפות כמו C או C++, שבהן אנחנו חייבים לנהל את הזיכרון ידנית (להקצות ולשחרר באמצעות malloc/free או delete), ב-Java יש מנגנון אומפטי שנקרא Garbage Collector. ה JVM עוקב אחר האובייקטים בזיכרון, מזהה אילו אובייקטים כבר אינם בשימוש (כלומר, אין אליהם יותר גישה או הפניות מאף מקום בקוד), ומשחרר את הזיכרון שהם תפסו.
מתי ה GC עובד?
ה GC ב Java עובד מאחורי הקלעים באופן אוטומטי, והוא מנהל את הזיכרון על בסיס הנחה מוכחת שנקראת ההיפוטזה הגנרטיבית החלשה (Weak Generational Hypothesis) רוב האובייקטים מתים צעירים מאוד
איך ה JVM משתמש בזה?
במקום שה GC יסרוק כל פעם מחדש את כל הזיכרון באפליקציה (מה שלוקח המון זמן ועוצר את התוכנית), ה JVM מחלק את ה Heap (הזיכרון) לאזורים לפי גיל האובייקטים דורות (Generations):
- דור הצעירים (Young Generation): לשם נכנסים כל האובייקטים החדשים. מכיוון שעל פי ההיפוטזה רובם ימותו מהר, ה GC סורק את האזור הזה בתדירות גבוהה מאוד (Minor GC). רוב הזמן הוא רק אוסף את מעט האובייקטים ששרדו, מעביר אותם הלאה, ומנקה את השאר בצורה מהירה ויעילה בלי לגעת בשאר הזיכרון.
- דור המבוגרים (Old / Tenured Generation): אובייקטים ששרדו מספיק "סבבים" בדור הצעירים מקודמים לכאן. מכיוון שהם כנראה ימשיכו לחיות עוד הרבה זמן, ה GC סורק את האזור הזה בתדירות נמוכה בהרבה (Major GC / Full GC).
החלוקה הזו חוסכת למערכת משאבים עצומים, כי היא מונעת מה GC לבזבז זמן יקר על סריקת אובייקטים ישנים שלא הולכים לשום מקום.
האם ניתן לפנות זיכרון ידנית, האם ניתן ל"קרוא" ל GC?
התשובה הקצרה היא: לא! אבל ניתן לכתוב כמה מונפולציות בקוד שאולי "יקראו" ל GC.
אין ב Java פקודה כמו free(). עם זאת, ישנן שתי דרכים עקיפות שבהן מפתחים מנסים "לעזור" ל GC לרוץ:
- System.gc(): קריאה למתודה זו ממליצה ל JVM להריץ את ה GC. שימו לב! זו רק המלצה! ה JVM יכול להתעלם ממנה לחלוטין אם הוא החליט שזה לא הזמן המתאים. מומלץ כמעט תמיד להימנע משימוש בה בפרודקשן, כי היא עלולה לגרום לעצירות מיותרות (Stop The World).
- איפוס הפניות (Nullifying references): אם נקבע null למשתנה שמחזיק אובייקט ענק וכבד שאינכם צריכים עוד, אתם בעצם "מנתקים" את ההפנייה ובכך מאפשרים ל GC לזהות את האובייקט כ"זמין לפינוי" בריצה הבאה.
GC בראי הזמן והתקדמות גרסאות JAVA :
בעולם הישן (Java 8 ומטה):
- ברירת המחדל הייתה Parallel GC.
- אם האפליקציה שלנו צרכה המון זיכרון (למשל 32GB ומעלה), ה GC היה צריך לעצור מדי פעם את הכל (Stop The World) כדי לסרוק את הכל, מה שהיה גורם ל"תקיעות" או השהיות מורגשות (Latency) שיכלו להגיע לשניות שלמות.
בעולם המודרני (Java 17 ו 21):
- ב Java 17 ברירת המחדל כבר הייתה G1 GC היעיל יותר.
- ב Java 21 קיבלנו את ה-ZGC המשודרג (Generational ZGC), שמצליח לנהל זיכרונות ענקיים בלי שהמשתמש או הלקוח ירגישו בכלל שה GC עובד ברקע, כי העצירות נמדדות במיקרו שניות בודדות.
ככל שנתקדם לגרסת Java מתקדמת יותר, מנגנון הפינוי נהיה "חכם" יותר, עובד יותר ברקע במקביל לקוד שלנו, וחוסך לנו צורך לנסות ( וזה אכן ניסיון! כי אין לנו שליטה אמיתית עליו) ולכוון הגדרות זיכרון ידניות.
טבלת אלגוריתמי ה-GC
| אלגוריתם ה-GC | מתי הוצג / באיזו גרסת Java | המאפיין המרכזי | למי הוא מתאים? |
|---|---|---|---|
| Serial GC | Java 1.0 (ישן מאוד) | עובד ב Thread יחיד, מקפיא לחלוטין את כל האפליקציה בזמן הפינוי (Stop The World מלא). | אפליקציות קטנות מאוד, מכשירים עם מעבד יחלש ומעט זיכרון. |
| Parallel GC | Java 5 (והיה ברירת המחדל עד Java 8) | מנצל מספר מעבדים במקביל כדי לנקות את הזיכרון מהר יותר. הדגש פה הוא על תפוקה (Throughput) ולא על עצירות קצרות. | מערכות Backend כבדות שבהן חשוב שהמעבד יעבוד שיא הכוח, ולא אכפת להן מעצירות פתאומיות של שברירי שנייה עד כמה שניות. |
| G1 GC (Garbage-First) | הוצג ב Java 7, הפך לברירת המחדל מ Java 9 ועד Java 21 | מחלק את הזיכרון למקטעים קטנים (Regions) ובודק קודם כל את המקטעים עם הכי הרבה "זבל". מנוהל כך שיעמוד ב"יעדי עצירה" מוגדרים מראש. | רוב האפליקציות המודרניות הסטנדרטיות שדורשות איזון טוב בין ביצועים לעצירות סבירות. |
| ZGC | הוצג כניסיוני ב Java 11, הפך רשמי ויציב בגרסאות מתקדמות (ובוצעה לו מהפכה ב Java 21) | אלגוריתם אולטרה מהיר. רוב העבודה שלו נעשית במקביל (Concurrent) כשהאפליקציה ממשיכה לרוץ, כמעט בלי עצירות Stop The World. | מערכות ענק שדורשות זמן תגובה אפסי (Sub millisecond) גם תחת נפחי זיכרון עצומים של מאות ג'יגה בייט. |