טרמינולוגיה של הזיכרון

Meggin Kearney
Meggin Kearney

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

המונחים והמושגים שמתוארים כאן מתייחסים לכלי לניתוח ביצועי הערימה (heap Profiler) של כלי הפיתוח ב-Chrome. אם עבדתם בעבר עם Java, .NET או כלי אחר לניתוח זיכרון, יכול להיות שהרענון הזה יעזור לכם.

גדלים של אובייקטים

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

ייצוג חזותי של הזיכרון

אובייקט יכול לשמור את הזיכרון בשתי דרכים:

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

כשעובדים עם ה-heap Profiler ב'כלי פיתוח' (כלי לחקירת בעיות זיכרון שנמצאו בקטע 'פרופילים'), סביר להניח שאתם מעיינים במספר עמודות מידע שונות. שתי הווריאציות הבולטות הן Shallow גודל ו-Retained size, אבל מה הן מייצגות?

גודל רדוד ונשמר

גודל רדוד

זה גודל הזיכרון שהאובייקט עצמו מחזיק.

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

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

הגודל שנשמר

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

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

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

  • אובייקט גלובלי של חלון (בכל iframe). בקובצי ה-snapshot של הערימה יש שדה מרחק, שהוא מספר ההפניות של המאפיינים בנתיב השימור הקצר ביותר מהחלון.
  • עץ DOM של מסמך שמכיל את כל צומתי ה-DOM המקוריים שניתן להגיע אליהם על ידי מעבר במסמך. לא לכולם יש wrappers של JS, אבל אם הם כוללים את ה-wrappers יהיו פעילים בזמן שהמסמך פעיל.
  • לפעמים אובייקטים נשמרים באמצעות ההקשר של הכלי לניפוי באגים ומסוף כלי הפיתוח (למשל, אחרי הערכה של המסוף). יצירת תמונות מצב של הזיכרון (heap snapshot) עם מסוף ברור וללא נקודות עצירה פעילות בכלי לניפוי באגים.

גרף הזיכרון מתחיל ברמה הבסיסית (root), שיכול להיות window של הדפדפן או האובייקט Global של מודול Node.js. אין לך שליטה על האופן שבו אובייקט הבסיס הזה מקבל GC.

לא ניתן לשלוט באובייקט ברמה הבסיסית (root)

מה שלא ניתן להגיע אליו מהרמה הבסיסית (root) מקבל GC.

אובייקטים השומרים על עץ

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

  • תווית הצמתים (או האובייקטים) היא השם של הפונקציה constructor ששימשה ליצירת אותם.
  • שוליים מסומנים באמצעות שמות הנכסים.

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

מרחק מהשורש

דומינטורים

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

בתרשים הבא:

  • צומת 1 שולט בצומת 2
  • צומת 2 שולט בצמתים 3, 4 ו-6
  • צומת 3 שולט בצומת 5
  • צומת 5 שולט בצומת 8
  • צומת 6 שולט בצומת 7

מבנה עץ הדומינטור

בדוגמה הבאה, הצומת #3 הוא השולט של #10, אבל #7 קיים גם בכל נתיב פשוט מ-GC אל #10. לכן, אובייקט B הוא דומינטור של אובייקט A אם B קיים בכל נתיב פשוט מהשורש לאובייקט A.

איור של דומינטור עם אנימציה

פרטים על V8

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

ייצוג אובייקט JavaScript

יש שלושה סוגים של פרימיטיביות:

  • מספרים (למשל 3.14159..)
  • ערך בוליאני (TRUE או FALSE)
  • מחרוזות (למשל 'וורנר הייזנברג')

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

אפשר לשמור מספרים בתור:

  • ערכים של מספרים שלמים מיידיים בגרסת 31 ביט שנקראים מספרים שלמים קטנים (SMI), או
  • אובייקטים של ערימה (heap), המכונים מספרי ערימה. מספרי ערימה (heap) משמשים לאחסון ערכים שלא מתאימים לטופס ה-SMI, למשל doubles, או כשצריך לכניס את הערך לקופסה, למשל להגדיר מאפיינים (properties).

אפשר לאחסן מחרוזות באחת מהדרכים הבאות:

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

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

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

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

לדוגמה, אם משרשרים את a ו-b, מקבלים מחרוזת (a, b) שמייצגת את תוצאת השרשור. אם בהמשך משרשרים את d לתוצאה הזו, מקבלים מחרוזת נוספת של חסרונות ((a, b), d).

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

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

  • מאפיינים בעלי שם, וגם
  • אלמנטים מספריים

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

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

קבוצות אובייקטים

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

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