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

Meggin Kearney
Meggin Kearney

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

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

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

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

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

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

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

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

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

גודל שטחי

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

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

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

הנפח שמתפנה (retained size)

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

שורשי GC מורכבים מאחזים שנוצרים (מקומיים או גלובליים) כשיוצרים הפניה מקוד מקומי לאובייקט JavaScript מחוץ ל-V8. כל הפקדים האלה נמצאים בתוך קובץ snapshot של אשכול, בקטע GC roots‏ > Handle scope ובקטע GC roots‏ > Global handles. תיאור הלחצנים במסמך הזה בלי להיכנס לפרטים של ההטמעה בדפדפן עלול לבלבל. אין צורך לדאוג לגבי השורשים של GC וגם לגבי ה-handles.

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

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

תרשים הזיכרון מתחיל בשורש, שיכול להיות אובייקט window של הדפדפן או אובייקט Global של מודול Node.js. אין לכם שליטה על האופן שבו אובייקט הבסיס הזה יועבר ל-GC.

לא ניתן לשלוט באובייקט ברמה הבסיסית.

כל מה שלא ניתן לגשת אליו מהשורש עובר ניהול זיכרון.

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

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

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

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

דוגמה למרחק מהשורש.

דומיננטים

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

בתרשים הבא:

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

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

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

איור מונפש של 'דומינטור'.

פרטים ספציפיים לגבי V8

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

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

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

  • מספרים (למשל, 3.14159..)
  • ערכים בוליאניים (true או false)
  • מחרוזות (למשל, 'Werner Heisenberg')

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

אפשר לשמור מספרים כ:

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

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

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

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

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

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

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

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

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

  • נכסים עם שם,
  • רכיבים מספריים

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

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

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

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

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