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

מגין קירני
מגין קירני

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

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

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

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

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

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

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

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

גודל רדוד ומוחזר

גודל שטחי

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

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

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

הגודל נשמר

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

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

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

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

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

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

פריט שלא ניתן להגיע אליו מהשורש יקבל 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..)
  • ערך בוליאני (נכון או לא נכון)
  • מחרוזות (למשל, 'וורנר הייזנברג')

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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