Sizeof עבור Java

26 בדצמבר 2003

ש: האם ל- Java יש אופרטור כמו sizeof () ב- C?

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

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

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

כמובן, זה עובד רק עבור יישומי Java פשוטים. בהשוואה ל- C / C ++, מבני נתונים מקבילים של Java נוטים לתפוס יותר זיכרון פיזי. בפיתוח תוכנה ארגונית, התקרבות לזיכרון הווירטואלי המרבי הזמין במכשירי JVM של 32 סיביות כיום היא אילוץ מדרגי שכיח. לפיכך, מתכנת Java יכול להרוויח sizeof()או משהו דומה כדי לפקוח עין אם מבני הנתונים שלו הולכים וגדלים או מכילים צווארי בקבוק זיכרון. למרבה המזל, השתקפות ג'אווה מאפשרת לך לכתוב כלי כזה די בקלות.

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

כשל: אין צורך ב- Sizeof () מכיוון שגדלי סוגי הבסיס של Java קבועים

כן, Java intהוא 32 ביט בכל ה- JVM ובכל הפלטפורמות, אך זוהי רק דרישת מפרט שפה עבור רוחב הנתפס למתכנת מסוג נתונים זה. סוג כזה intהוא למעשה סוג נתונים מופשט וניתן לגבות אותו, למשל, מילת זיכרון פיזית של 64 סיביות במכונת 64 סיביות. כנ"ל לגבי סוגים שאינם פרימיטיביים: מפרט שפת Java אינו אומר דבר על אופן יישור שדות הכיתה בזיכרון הפיזי או על כך שלא ניתן היה ליישם מערך של בוליאנים כקטור ביט קומפקטי בתוך ה- JVM.

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

הסיבה שזה לא עובד היא כי פריסת הסידור היא רק השתקפות מרחוק של הפריסה האמיתית בזיכרון. אחת הדרכים הקלות לראות זאת היא על ידי התבוננות כיצד ניתן Stringלקבל סדרתי: בזיכרון כל אחד מהם charהוא לפחות שני בתים, אך בצורה סדרתית Stringמקודדים UTF-8 ולכן כל תוכן ASCII לוקח שטח רב יותר.

גישת עבודה נוספת

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

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

מה גודל האובייקט?

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

  • ניתן לגודל (בערך) מופע של אובייקט על ידי סה"כ כל שדות הנתונים הלא סטטיים שלו (כולל שדות המוגדרים בכיתות-על)
  • שלא כמו, נניח, C ++, לשיטות מחלקה ולווירטואליות שלהן אין השפעה על גודל האובייקט
  • לממשקי-על של הכיתה אין השפעה על גודל האובייקט (ראו ההערה בסוף הרשימה הזו)
  • ניתן להשיג את גודל האובייקט המלא כסגירה על כל גרף האובייקט המושרש באובייקט ההתחלתי
הערה: יישום ממשק Java כלשהו מסמן את הכיתה המדוברת בלבד ואינו מוסיף נתונים להגדרה שלה. למעשה, ה- JVM אינו מאמת אפילו כי יישום ממשק מספק את כל השיטות הנדרשות על ידי הממשק: זו אחריות המהדר בהחלט במפרט הנוכחי.

כדי לאתחל את התהליך, עבור סוגי נתונים פרימיטיביים אני משתמש בגדלים פיזיים כפי שנמדד לפי Sizeofמחלקה של Java Tip 130 . כפי שמתברר, עבור JVM נפוצים של 32 סיביות רגיל java.lang.Objectתופס 8 בתים, וסוגי הנתונים הבסיסיים הם בדרך כלל בגודל הפחות פיזי שיכול להתאים לדרישות השפה (למעט booleanלוקח בתים שלמים):

// java.lang. גודל מעטפת האובייקט בבייטים: ציבורי סטטי ציבורי int OBJECT_SHELL_SIZE = 8; גמר סטטי ציבורי int OBJREF_SIZE = 4; גמר סטטי ציבורי int int LONG_FIELD_SIZE = 8; גמר סטטי ציבורי int INT_FIELD_SIZE = 4; גמר סטטי ציבורי int int SHORT_FIELD_SIZE = 2; גמר סטטי ציבורי ציבורי int CHAR_FIELD_SIZE = 2; גמר סטטי ציבורי int BYTE_FIELD_SIZE = 1; גמר סטטי ציבורי int BOOLEAN_FIELD_SIZE = 1; גמר סטטי ציבורי int DOUBLE_FIELD_SIZE = 8; גמר סטטי ציבורי int int FLOAT_FIELD_SIZE = 4;

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

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

ממשק IObjectProfileNode {אובייקט אובייקט (); שם מחרוזת (); גודל int (); int refcount (); הורה IObjectProfileNode (); IObjectProfileNode [] ילדים (); פגז IObjectProfileNode (); נתיב IObjectProfileNode [] (); שורש IObjectProfileNode (); אורך נתיב int (); מעבר בוליאני (מסנן INodeFilter, מבקר INodeVisitor); מזבלה מחרוזת (); } // סוף הממשק

IObjectProfileNodes מחוברים זה לזה כמעט בדיוק באותו אופן כמו גרף האובייקט המקורי, עם IObjectProfileNode.object()החזרת האובייקט האמיתי שכל צומת מייצג. IObjectProfileNode.size()מחזיר את הגודל הכולל (בבייטים) של עצם המשנה של האובייקט המושרש במופע האובייקט של אותו צומת. אם מופע אובייקט מקשר לאובייקטים אחרים באמצעות שדות מופע שאינם אפסים או באמצעות הפניות הכלולים בשדות מערך, אז IObjectProfileNode.children()תהיה רשימה מתאימה של צמתים של גרפי צאצא, הממוינים בסדר גודל יורד. לעומת זאת, עבור כל צומת שאינו ההתחלתי, IObjectProfileNode.parent()מחזיר את האב שלו. כל אוסף ה- IObjectProfileNodes פורס וקובץ את האובייקט המקורי ומראה כיצד מחולקים בתוכו אחסון נתונים. יתר על כן, שמות צומת הגרפים נגזרים משדות המחלקה ובוחנים את נתיב הצומת בתוך הגרף (IObjectProfileNode.path()) מאפשר לך לעקוב אחר קישורי הבעלות ממופע האובייקט המקורי לכל פיסת נתונים פנימית.

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

 אובייקט obj = מחרוזת חדשה [] {מחרוזת חדשה ("JavaWorld"), מחרוזת חדשה ("JavaWorld")}; 

לכל java.lang.Stringמופע יש שדה פנימי מסוג char[]שהוא תוכן המחרוזת בפועל. באופן בו Stringבונה ההעתקות עובד ב- Java 2 Platform, מהדורה רגילה (J2SE) 1.4, שני Stringהמופעים בתוך המערך הנ"ל ישתפו באותו char[]מערך המכיל את {'J', 'a', 'v', 'a', 'W', 'o', 'r', 'l', 'd'}רצף התווים. שני המיתרים הם בעלי מערך זה באותה מידה, אז מה עליכם לעשות במקרים כאלה?

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

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

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