גיבוב אשפה ב-Java הוא אחת מהתחומים המתקדמים. ידע ב-GC של Java עוזר לנו להגדיר בצורה מדויקת את ביצועי האפליקציה שלנו בזמן ריצה.
גיבוב אשפה ב-Java
- ב-Java, המתכנתים אינם צריכים לדאוג להרוס את האובייקטים שאינם בשימוש. האוסף האשפה מתעסק בזה.
- אוסף האשפה הוא Thread דמון הפועל ברקע. בגדול, הוא שחרר את זיכרון ה-heap על ידי הרס אובייקטים שאינם נגישים.
- אובייקטים שאינם נגישים הם אלה שאינם משויכים עוד לכל חלק מהתוכנית.
- ניתן לבחור את מנהל האשפה לתוכנית ה-Java שלנו דרך אפשרויות ה-JVM, נתעמק בכך בחלק מאוחר יותר במדריך זה.
איך גיבוב אשפה אוטומטי עובד?
האיסוף האוטומטי של זבל הוא תהליך שבו נבדקת זכרון ההיפ, מזהה (נקרא גם "סימון") את האובייקטים שאי אפשר לגשת אליהם, ומשמיד אותם בעזרת דחיסה. בעיה בשיטה זו היא שככל שמספר האובייקטים גדול, זמן האיסוף של הזבל ממשיך לגדול, מכיוון שצריך לעבור דרך רשימת האובייקטים במלואה, בחיפוש אחר האובייקט הלא נגיש. אך, הניתוח האמפירי של היישומים מראה כי רוב האובייקטים הם קצרי חיים. ההתנהגות זו נעשתה שימוש בה לשיפור ביצועי הJVM, והמתודה שאומצה נקראת בדרך כלל איסוף זבל דורי. בשיטה זו, המרחב של ההיפ מחולק לדורות כמו הדור הצעיר, הדור הישן או הזקן, והדור הקבוע. מרחב ההיפ של הדור הצעיר הוא החדש שבו נוצרים כל האובייקטים החדשים. לאחר שהוא מתמלא, מתרחש איסוף זבל מינורי (נקרא גם GC מינורי). מה שאומר, כל האובייקטים המתים מדור זה מושמדים. התהליך הזה מהיר מאוד מכיוון שכמו שאנו יכולים לראות מהגרף, רובם יהיו מתים. האובייקטים השורדים בדור הצעיר מזדקנים ובסופו של דבר מתקדמים לדורות המבוגרים. הדור הישן משמש לאחסון אובייקטים שחיים לאורך זמן. לרוב, נקבע סף עבור אובייקטי הדור הצעיר וכאשר הגיל הזה מתקיים, האובייקט מועבר לדור הישן. בסופו של דבר, נדרש לאסוף את הדור הישן. אירוע זה נקרא GC ראשי (איסוף זבל ראשי). לעיתים קרובות זה איטי יותר מכיוון שהוא כולל את כל האובייקטים החיים. גם קיים GC מלא, שפונה לניקוי של כל ההיפ – הכולל גם את מרחבי הדור הצעיר והמבוגרים. לבסוף, עד ל-Java 7, היה דור קבוע (או דור Perm), שהכיל מטא-נתונים דרושים על ידי הJVM כדי לתאר את המחלקות והשיטות שנעשה בהן שימוש ביישום. נמחק ב-Java 8.
אוספי הזבל של ג'אווה
הJVM מספק בעצם ארבעה אוספי זבל שונים, כולם דוריים. לכל אחד יתרונותיו וחסרונותיו הם. הבחירה של איזה אוסף זבל להשתמש בו נמצאת בידינו ויכולה להיות הבדלים דרמטיים בפעולת המעבר ובעצירותיו של היישום. כל אלה, חולקים את הערימה הניהולית לחלקים שונים, בהנחות העתיקות שרוב האובייקטים בערימה הם חיים קצרים וצריכים להישמש מהר. לכן, ארבעת סוגי אוספי הזבל הם:
זבל סיריאלי
זהו האוסף הפשוט ביותר, מיועד למערכות בשרשור יחיד ולגודל ערימה קטן. הוא מקפיא את כל היישומים במהלך העבודה. ניתן להפעיל אותו באמצעות אפשרות JVM -XX:+UseSerialGC
.
זבל פרללי/תפוקה
זהו סוכן האיסוף המוגדר כברירת מחדל של JVM ב־ JDK 8. כפי שהשם מרמז, הוא משתמש במספר תהליכים לסריקה דרך מרחב ההשקה ובביצוע קימפקציה. אחד החסרונות של סוכן זה הוא שהוא משהה את תהליכי היישום בזמן ביצוע GC מינורי או מלא. הוא מתאים ביותר ליישומים שיכולים לטפל בהשהיות כאלה ומנסים לאופטימיזציה של העומס של המעבד.
סוכן CMS
אלגוריתם סוכן CMS ("concurrent-mark-sweep") משתמש במספר תהליכים ("concurrent") לסריקה דרך ההשקה ("mark") לאובייקטים שאינם בשימוש שניתן למחזור ("sweep"). סוכן זה נכנס למצב Stop-The-World (STW) בשני מקרים: – בעת אתחול הסימון הראשוני של שורשים, כלומר, אובייקטים בדור הישן שנגישים מנקודות הכניסה של התהליכים או משתנים סטטיים – כאשר היישום שינה את מצב ההשקה בזמן שהאלגוריתם פועל באופן זמני ומאלץ אותו לחזור ולבצע מגעים סופיים כדי לוודא שיש לו את האובייקטים הנכונים מסומנים. סוכן זה עשוי להתמודד עם כשלים בקידום. אם ישנם אובייקטים מהדור הצעיר שיש להעביר לדור הישן, והסוכן אינו מספיק זמן כדי ליצור מקום במרחב של דור הישן, כשל בקידום יתרחש. כדי למנוע זאת, ניתן לספק יותר מרווח למרחב של ההשקה הישן או לספק יותר תהליכי רקע לסוכן.
אוסף G1
אחרונה אך לא פחות חשובה היא אוסף Garbage-First, המותאם לגדלי זיכרון גדולים מ-4GB. הוא מחלק את גודל הזיכרון לאזורים בין 1MB ל-32MB, בהתבסס על גודל הזיכרון. יש פה שלב של סימון גלובלי מתמשך לקבוע את החיוניותם של אובייקטים בכל הזיכרון. לאחר שהשלב של הסימון הואמץ, G1 יודע לאילו אזורים יש בעיקר פריקה. הוא אז יאסוף את האובייקטים שאי אפשר לגשת אליהם מתוך אותם אזורים ראשונים, מה שבדרך כלל מביא לשחרור של כמות גדולה של מקום פנוי. כך ש-G1 אוסף תחילה את אזורים אלו (המכילים אשפה), ולכן שמו "Garbage-First". G1 גם משתמש במודל חיזוי עצירה כדי להגיע ליעד זמן עצירה שהמשתמש הגדיר. הוא בוחר את מספר האזורים לאיסוף בהתאם ליעד הזמן לעצירה הספורץ. מחזור איסוף האשפה של G1 כולל את השלבים כפי שמוצג באיור:
-
שלב רק עבור צעירים: שלב זה כולל רק אובייקטים בדור הצעירים ומקדם אותם לדור הישן. המעבר בין שלב הצעירים לבין שלב השחרור של המרחב מתחיל כאשר דור הישן מתמלא עד לסף מסוים, כלומר, סף המילוי המתחיל. בעת זו, G1 יזמין את האספקט הרק עבור צעירים במקום אספקט רגיל של קולקציה רק עבור צעירים.
-
שלב צעירים בלבד: שלב זה כולל רק אובייקטים מהדור הצעירה וקודם להם לדור הזקן. המעבר בין שלב הצעירים בלבד ושלב אחזקת המרחק מתחיל כאשר הדור הזקן תפוס עד לסף מסוים, כלומר סף התפקוד של קופה ההתחלה. בזמן זה, G1 קובעת לאסוף את הצעירים עם סימון ראשוני במקום אסוף רגיל של הצעירים.
-
סימון ראשוני: סוג זה של אסוף מתחיל את תהליך הסימון יחד עם אסוף רגיל של הצעירים. הסימון הקרינטי מקבע את כל האובייקטים החיים כעת באזורי הדור הזקן שישמרו לשלב אחזקת המרחק לאחר מכן. בזמן שהסימון אינו סיים לגמרי, ייתכנו אסופות רגילות של הצעירים. הסימון מסתיים עם שני עצורים מיוחדים של עולמית: הערה וניקיון.
-
ניקיון: ההשהייה הזו גם מתעסקת באזורים הריקים לחלוטין, ומקבעת האם יתרחש שלב של חידוש מרחב אכן אחר. אם שלב חידוש המרחב מתרחש, שלב המיועד לדור הצעיר מסתיים עם איסוף יחיד של דור צעיר בלבד.
-
שלב חידוש מרחב: השלב הזה מורכב מאוסף אוספים מעורבים – בנוסף לאזורי הדור הצעיר, גם מאכסן עצמים חיים מאזורי הדור הישן. שלב החידוש מרחב מסתיים כאשר G1 מחליט שהאכסון של עוד אזורי דור ישן לא יעניק מספיק מקום פנוי שווה למאמץ.
G1 ניתן להפעיל באמצעות הדגל -XX:+UseG1GC
. אסטרטגיה זו מפחיתה את הסיכויים של פיצוץ הערימה לפני שלילת הלשוניים של הרקע הושלמו בסריקת אובייקטים שאי אפשר לגשת אליהם. בנוסף, היא דוחפת את הערימה במהלך הרצתה, מה שהמאסף CMS יכול לעשות רק במצב הסטופ-וואורד. ב-Java 8 קיימת אופטימיזציה נפלאה עם מאסף G1, שנקראת אחסון מחרוזות. כפי שאנו יודעים, מערכות התווים שמייצגות את המחרוזות שלנו תופסות רוב מרבית מהמקום בערימה שלנו. בוצעה אופטימיזציה חדשה שמאפשרת למאסף G1 לזהות מחרוזות שמשוכפלות יותר מפעם אחת בכל ערימה שלנו ולשנות אותן כך שיפנו לאותו מערך תווים פנימי, כדי למנוע העתקים מרובים של אותה מחרוזת השוכנים בערימה מיותרים. ניתן להשתמש בארגומנט JVM -XX:+UseStringDeduplication
כדי להפעיל את האופטימיזציה הזו. G1 הוא מאסף הזבל הברירת מחדל ב-JDK 9.
Java 8 PermGen ו-Metaspace
כפי שאומר למעלה, נמחק המרחב הדילוג (Permanent Generation) מאז Java 8. כעת, JDK 8 HotSpot JVM משתמשת בזיכרון טבעי לייצוג של מטא-מידע שמזוהה בשם Metaspace. רוב ההקצאות של מטא-מידע לכיתות נעשות מזיכרון טבעי. יש גם דגל חדש בשם MaxMetaspaceSize שמגביל את כמות הזיכרון המשמשת למטא-מידע לכיתות. אם לא נציין ערך לדגל זה, ה-Metaspace משנה גודל בזמן ריצת היישום על פי דרישת היישום הרץ. איסוף זבל ב-Metaspace מתרחש כאשר שימוש במטא-מידע לכיתות מגיע למגבלת MaxMetaspaceSize. איסוף זבל מיותר ב-Metaspace עשוי להיות סימפטום של נזילה של זיכרון לכיתות, מנגנון טעינה של כיתות שנשמר בזיכרון, או גודל לא מתאים ליישום שלנו. זהו הכל בנוגע לאיסוף זבל ב-Java. אני מקווה שהבנת את ההסבר על אוספי הזבל השונים שיש לנו ב-Java. המקורות: תיעוד של Oracle, G1 GC.
Source:
https://www.digitalocean.com/community/tutorials/garbage-collection-in-java