מבוא ל- ZGC: אספן אשפה JVM בעל יכולת הרחבה נמוכה וניתנת להרחבה

1. הקדמה

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

כדי לטפל בבעיה זו, Java 11 הציגה את Z Garbage Collector (ZGC) כמימוש אספן אשפה ניסיוני (GC).

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

2. מושגים עיקריים

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

2.1. ניהול זיכרון

זיכרון פיזי הוא ה- RAM שמספק החומרה שלנו.

מערכת ההפעלה (OS) מקצה שטח זיכרון וירטואלי לכל יישום.

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

2.2. מיפוי רב

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

למעשה, אנו ממפים טווחים מרובים של הזיכרון הווירטואלי לאותו טווח בזיכרון הפיזי:

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

2.3. רילוקיישן

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

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

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

2.4. איסוף זבל

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

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

כדי להשיג זאת, לאוספי האשפה יש שלבים מרובים.

2.5. מאפייני שלב GC

לשלבי GC יכולים להיות מאפיינים שונים:

  • א מַקְבִּיל שלב יכול לפעול על מספר אשכולות GC
  • א סידורי שלב פועל על חוט יחיד
  • א עצור את העולם שלב אינו יכול לפעול במקביל לקוד היישום
  • א במקביל שלב יכול לרוץ ברקע, בעוד היישום שלנו עושה את עבודתו
  • an מצטבר השלב יכול להסתיים לפני סיום כל עבודתו ולהמשיך בהמשך

שימו לב שלכל הטכניקות הנ"ל יש את נקודות החוזק והחולשה שלהן. לדוגמה, נניח שיש לנו שלב שיכול לרוץ במקביל ליישום שלנו. יישום סדרתי של שלב זה דורש 1% מביצועי המעבד הכללי ופועל למשך 1000 ms. לעומת זאת, יישום מקביל מנצל 30% מהמעבד ומשלים את עבודתו ב 50ms.

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

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

לתיאורים מפורטים יותר, בקר במאמר שלנו בנושא ניהול זיכרון Java.

3. מושגי ZGC

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

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

אבל לעת עתה, בואו נסתכל על התמונה הכוללת של אופן הפעולה של ZGC.

3.1. תמונה גדולה

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

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

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

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

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

3.2. צִיוּן

ZGC מפרק את הסימון לשלושה שלבים.

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

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

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

בשלב זה אנו יודעים לאילו עצמים אנו יכולים להגיע.

ZGC משתמש ב- מסומן 0 ו מסומן 1 סיביות מטא נתונים לסימון.

3.3. צביעת הפניה

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

עם 32 ביט, אנו יכולים לטפל ב -4 ג'יגה. מכיוון שבימינו נפוץ למחשב שיהיה לו יותר זיכרון מזה, ברור שאנחנו לא יכולים להשתמש באף אחד מ -32 הסיביות הללו לצביעה. לכן, ZGC משתמש בהפניות של 64 סיביות. זה אומר ZGC זמין רק בפלטפורמות 64 סיביות:

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

נוסף על כך, יש לנו 4 ביטים לאחסון מצבי התייחסות:

  • ניתן לסיים bit - ניתן להגיע לאובייקט רק דרך קצה הגמר
  • למפות מחדש bit - ההתייחסות מעודכנת ומצביעה על המיקום הנוכחי של האובייקט (ראה רילוקיישן)
  • מסומן 0 ו מסומן 1 ביטים - אלה משמשים לסימון אובייקטים הנגישים

קראנו גם לסיביות אלה סיביות מטא-נתונים. ב- ZGC, בדיוק אחד מקטעי המטא נתונים האלה הוא 1.

3.4. רילוקיישן

ב- ZGC, רילוקיישן מורכב מהשלבים הבאים:

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

3.5. מיפוי מחדש ומחסומי עומס

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

ZGC משתמש במחסומי עומס כדי לפתור בעיה זו. מחסומי עומס מתקנים את ההפניות המפנות לאובייקטים שהועברו בטכניקה הנקראת remapping.

כאשר היישום טוען הפניה, הוא מפעיל את מחסום העומס, ולאחר מכן מבצע את השלבים הבאים להחזרת הפניה הנכונה:

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

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

4. כיצד להפעיל ZGC?

אנו יכולים לאפשר ZGC באמצעות אפשרויות שורת הפקודה הבאות בעת הפעלת היישום שלנו:

-XX: + UnlockExperimentalVMOptions -XX: + השתמש ב- ZGC

שים לב שמכיוון ש- ZGC הוא GC ניסיוני, ייקח זמן מה עד שיתמכו רשמית.

5. מסקנה

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

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


$config[zx-auto] not found$config[zx-overlay] not found