מיטוב ביצועי JVM, חלק 3: איסוף זבל

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

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

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

מיטוב ביצועי JVM: קרא את הסדרה

  • חלק 1: סקירה כללית
  • חלק 2: מהדרים
  • חלק 3: איסוף זבל
  • חלק 4: דחיסת GC במקביל
  • חלק 5: מדרגיות

אוסף זבל ומודל הזיכרון של פלטפורמת Java

כאשר אתה מציין את אפשרות ההפעלה -Xmxבשורת הפקודה של יישום Java שלך (למשל java -Xmx:2g MyApp:) זיכרון מוקצה לתהליך Java. זיכרון זה מכונה ערמת ג'אווה (או סתם ערימה ). זהו שטח כתובות הזיכרון הייעודי אליו יוקצו כל האובייקטים שנוצרו על ידי תוכנית ה- Java שלך (או לפעמים ה- JVM). מכיוון שתוכנית ה- Java שלך ממשיכה לפעול ולהקצות אובייקטים חדשים, ערמת Java (כלומר שטח הכתובת) תתמלא.

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

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

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

שני סוגים של איסוף אשפה

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

קרא את סדרת ייעול הביצועים של JVM

  • מיטוב ביצועי JVM, חלק 1: סקירה כללית
  • מיטוב ביצועי JVM, חלק 2: מהדרים

אספני ספירת הפניות

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

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

מעקב אחר אספנים

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

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

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

מעקב אחר אלגוריתמי אספנים

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

העתקת אספנים

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

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

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

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

חסרונות של העתקת אספנים

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

סמן וטאטא אספנים

מרבית מכוניות ה- JVM המסחריות הפרוסות בסביבות ייצור ארגוניות מפעילות אספני סימון וסחיפה (או סימון), שאין להם את השפעת הביצועים של אספני ההעתקה. כמה מאוספי הסימונים המפורסמים ביותר הם CMS, G1, GenPar ו- DeterministicGC (ראה משאבים).

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

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

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

מידע נוסף על מידות TLAB

חלוקת TLAB ו- TLA (חוצץ הקצאה מקומי או חוט מקומי חוט) נידונות באופטימיזציה של ביצועי JVM, חלק 1.

חסרונות של אספני סימון וטאטא

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

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

יישומים של סימון-וסוויפ

ישנן לפחות שתי גישות זמינות ומסחריות להטמעת אוסף סימון-וסוויפ. האחת היא הגישה המקבילה והשנייה היא הגישה במקביל (או בעיקר במקביל).

אספנים מקבילים

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