טיפ ג'אווה 130: האם אתה יודע את גודל הנתונים שלך?

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

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

הערה: ניתן להוריד את קוד המקור של מאמר זה מ- Resources.

הכלי

מכיוון שג'אווה מסתירה בכוונה היבטים רבים של ניהול זיכרון, גילוי כמה זיכרון האובייקטים שלך צורכים לוקח קצת עבודה. תוכלו להשתמש Runtime.freeMemory()בשיטה למדידת הבדלי גודל הערימה לפני ואחרי שהוקצו מספר אובייקטים. כמה מאמרים, כמו "שאלת השבוע מס '107" של רמכרנדר ורדארג'אן (Sun Microsystems, ספטמבר 2000) ו"ענייני הזיכרון "של טוני סינטס ( JavaWorld, דצמבר 2001), מתארים את הרעיון הזה. למרבה הצער, הפתרון של המאמר לשעבר נכשל מכיוון שהיישום משתמש Runtimeבשיטה שגויה , בעוד שלפתרון המאמר האחרון יש פגמים משלו:

  • שיחה אחת Runtime.freeMemory()להוכיח שאינה מספקת מכיוון ש- JVM עשוי להחליט להגדיל את גודל הערימה הנוכחי בכל עת (במיוחד כאשר הוא מפעיל איסוף אשפה). אלא אם גודל הערמה הכולל כבר בגודל המקסימלי -Xmx, עלינו להשתמש Runtime.totalMemory()-Runtime.freeMemory()כגודל הערמה המשומש.
  • ייתכן שביצוע Runtime.gc()שיחה בודדת לא יתגלה בתוקפנות מספקת לבקשת איסוף אשפה. נוכל, למשל, לבקש שיסיימו את האובייקט להפעלה גם כן. ומכיוון Runtime.gc()שאינו מתועד לחסימה עד לסיום האיסוף, מומלץ לחכות עד שמתייצב גודל הערימה.
  • אם המחלקה המופעלת יוצרת נתונים סטטיים כלשהם כחלק מהאתחול של המחלקה למחלקה שלה (כולל אתחול מחלקה סטטית ומתחילים בשדה), זיכרון הערימה המשמש למופע המחלקה הראשונה עשוי לכלול נתונים אלה. עלינו להתעלם משטח הערימה הנצרך על ידי המעמד הראשון.

בהתחשב בבעיות אלה, אני מציג Sizeofכלי איתו אני מחטט בכיתות הליבה והיישומים של Java:

class class Sizeof {public static void main (String [] args) throw exception {// לחמם את כל המחלקות / שיטות בהן נשתמש runGC (); זיכרון משומש (); // מערך כדי לשמור על הפניות חזקות לאובייקטים שהוקצו ספירת אינטל סופית = 100000; אובייקט [] אובייקטים = אובייקט חדש [ספירה]; ערימה ארוכה 1 = 0; // הקצה ספירה + 1 אובייקטים, מחק את הראשון עבור (int i = -1; i = 0) אובייקטים [i] = אובייקט; אחרת {object = null; // מחק את אובייקט החימום runGC (); heap1 = usedMemory (); // קח תמונת מצב לפני ערימה}} runGC (); ארוך heap2 = usedMemory (); // צלם תמונת מצב לאחר ערימה: גודל int סופי = Math.round (((float) (heap2 - heap1)) / count); System.out.println ("'לפני' ערימה:" + ערימה 1 + ", 'אחרי' ערימה:" + ערימה 2); System.out.println ("ערמת דלתא:" + (heap2 - heap1) + ", {" + אובייקטים [0].getClass () + "} size =" + size + "בתים"); עבור (int i = 0; i <count; ++ i) אובייקטים [i] = null; אובייקטים = null; } ריק סטטי פרטי runGC () זורק חריג {// זה עוזר להתקשר ל- Runtime.gc () // באמצעות מספר שיחות שיטה: עבור (int r = 0; r <4; ++ r) _runGC (); } ריק סטטי פרטי _runGC () זורק חריג {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; עבור (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} פרטית סטטית פרטית משומשת Memory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } סופי סטטי פרטי Runtime s_runtime = Runtime.getRuntime (); } // סוף השיעוראני <לספור; ++ i) אובייקטים [i] = null; אובייקטים = null; } ריק סטטי פרטי runGC () זורק חריג {// זה עוזר להתקשר ל- Runtime.gc () // באמצעות מספר שיחות שיטה: עבור (int r = 0; r <4; ++ r) _runGC (); } ריק סטטי פרטי _runGC () זורק חריג {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; עבור (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} פרטית סטטית פרטית משומשת Memory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } סופי סטטי פרטי Runtime s_runtime = Runtime.getRuntime (); } // סוף השיעוראני <לספור; ++ i) אובייקטים [i] = null; אובייקטים = null; } ריק סטטי פרטי runGC () זורק חריג {// זה עוזר להתקשר ל- Runtime.gc () // באמצעות מספר שיחות שיטה: עבור (int r = 0; r <4; ++ r) _runGC (); } ריק סטטי פרטי _runGC () זורק חריג {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; עבור (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} פרטית סטטית פרטית משומשת Memory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } סופי סטטי פרטי Runtime s_runtime = Runtime.getRuntime (); } // סוף השיעורgc () // תוך שימוש במספר קריאות שיטה: עבור (int r = 0; r <4; ++ r) _runGC (); } ריק סטטי פרטי _runGC () זורק חריג {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; עבור (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} פרטית סטטית פרטית משומשת Memory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } סופי סטטי פרטי Runtime s_runtime = Runtime.getRuntime (); } // סוף השיעורgc () // תוך שימוש במספר קריאות שיטה: עבור (int r = 0; r <4; ++ r) _runGC (); } ריק סטטי פרטי _runGC () זורק חריג {long usedMem1 = usedMemory (), usedMem2 = Long.MAX_VALUE; עבור (int i = 0; (usedMem1 <usedMem2) && (i <500); ++ i) {s_runtime.runFinalization (); s_runtime.gc (); Thread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} פרטית סטטית פרטית משומשת Memory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } סופי סטטי פרטי Runtime s_runtime = Runtime.getRuntime (); } // סוף השיעורThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} פרטית סטטית פרטית משומשת Memory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } סופי סטטי פרטי Runtime s_runtime = Runtime.getRuntime (); } // סוף השיעורThread.currentThread () .yield (); usedMem2 = usedMem1; usedMem1 = usedMemory (); }} פרטית סטטית פרטית משומשת Memory () {return s_runtime.totalMemory () - s_runtime.freeMemory (); } סופי סטטי פרטי Runtime s_runtime = Runtime.getRuntime (); } // סוף השיעור

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

שימו לב היטב למקומות בהם אני קורא runGC(). באפשרותך לערוך את הקוד בין ההצהרות heap1לבין heap2הצהרות כדי ליצור כל עניין שמעניין.

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

התוצאות

בואו נשתמש בכלי הפשוט הזה בכמה שיעורים ואז נראה אם ​​התוצאות תואמות את הציפיות שלנו.

הערה: התוצאות הבאות מבוססות על JDK 1.3.1 של Sun עבור Windows. בשל מה שמובטח ולא מובטח על ידי שפת Java ומפרטי JVM, אינך יכול להחיל תוצאות ספציפיות אלה על פלטפורמות אחרות או יישומי Java אחרים.

java.lang. אובייקט

ובכן, השורש של כל האובייקטים היה צריך להיות המקרה הראשון שלי. עבור java.lang.Object, אני מקבל:

'לפני' ערימה: 510696, 'אחרי' ערימה: 1310696 ערמת דלתא: 800000, {class java.lang.Object} size = 8 בתים 

אז, מישור Objectלוקח 8 בתים; כמובן, אף אחד לא יכול לצפות את הגודל להיות 0, כמו בכל מקרה חייב לסחוב שדות פעולות בסיס תמיכה אוהבות equals(), hashCode(), wait()/notify(), וכן הלאה.

java.lang. שלם

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

'לפני' ערימה: 510696, 'אחרי' ערימה: 2110696 ערמת דלתא: 1600000, {class java.lang.Integer} size = 16 bytes 

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

java.lang. ארוך

Longצריך לקחת יותר זיכרון מאשר Integer, אבל זה לא:

ערימה 'לפני': 510696, 'אחרי' ערימה: 2110696 ערמת דלתא: 1600000, {class java.lang.Long} size = 16 בתים 

ברור כי גודל האובייקט בפועל בערימה כפוף ליישור זיכרון ברמה נמוכה שנעשה על ידי יישום JVM מסוים עבור סוג מעבד מסוים. נראה כי a Longהוא 8 בתים של Objectתקורה, ועוד 8 בתים לערך הארוך בפועל. לעומת זאת, Integerהיה לו חור בן 4 בתים שלא נעשה בו שימוש, ככל הנראה מכיוון ש- JVM I משתמש בכוחות יישור אובייקט בגבול מילים של 8 בתים.

מערכים

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

אורך: 0, {מחלקה [I} גודל = 16 בתים אורך: 1, {מחלקה [I} גודל = 16 בתים אורך: 2, {מחלקה [I} גודל = 24 בתים אורך: 3, {מחלקה [I} גודל = אורך 24 בתים: 4, {מחלקה [I} גודל = 32 בתים אורך: 5, {מחלקה [I} גודל = 32 בתים אורך: 6, {מחלקה [I} גודל = 40 בתים אורך: 7, {מחלקה [I} גודל = 40 בתים אורך: 8, {מחלקה [I} גודל = 48 בתים אורך: 9, {מחלקה [I} גודל = 48 בתים אורך: 10, {מחלקה [I} גודל = 56 בתים 

ועל charמערכים:

אורך: 0, {מחלקה [C} גודל = 16 בתים אורך: 1, {מחלקה [C} גודל = 16 בתים אורך: 2, {מחלקה [C} גודל = 16 בתים אורך: 3, {מחלקה [C} גודל = אורך 24 בתים: 4, {class [C} גודל = 24 בתים אורך: 5, {class [C} size = 24 בתים אורך: 6, {class [C} size = 24 בתים אורך: 7, {class [C} גודל = 32 בתים אורך: 8, {מחלקה [C} גודל = 32 בתים אורך: 9, {מחלקה [C} גודל = 32 בתים אורך: 10, {מחלקה [C} גודל = 32 בתים 

למעלה, הראיות ליישור 8 בתים צצות שוב. כמו כן, בנוסף Objectלתקורה בת 8 הבתים הבלתי נמנעת , מערך פרימיטיבי מוסיף עוד 8 בתים (מתוכם לפחות 4 בתים תומכים lengthבשדה). int[1]נראה כי השימוש אינו מציע יתרונות זיכרון על פני Integerמופע, למעט אולי כגרסה משתנה של אותם נתונים.

מערכים רב מימדיים

מערכים רב מימדיים מציעים הפתעה נוספת. מפתחים משתמשים בדרך כלל במבנים כמו int[dim1][dim2]במחשוב מספרי ומדעי. בראיון int[dim1][dim2]למשל מערך, כל מקוננות int[dim2]מערך היא Objectבזכות עצמה. כל אחד מהם מוסיף את התקורה הרגילה של מערך 16 בתים. כשאני לא צריך מערך משולש או מרופט, זה מייצג תקורה טהורה. ההשפעה גוברת כאשר ממדי המערך שונים מאוד. לדוגמה, int[128][2]מופע לוקח 3,600 בתים. בהשוואה ל -1,040 בתים int[256]שבהם משתמש מופע (שיש לו אותה קיבולת), 3,600 בתים מייצגים תקורה של 246 אחוזים. במקרה הקיצוני של byte[256][1], גורם התקורה הוא כמעט 19! השווה זאת למצב C / C ++ בו אותו תחביר אינו מוסיף תקורה לאחסון.

מיתר

בואו ננסה ריק String, שנבנה תחילה כ new String():

'לפני' ערימה: 510696, 'אחרי' ערמה: 4510696 ערמת דלתא: 4000000, {class java.lang.String} size = 40 בתים 

התוצאה מוכיחה די מדכאת. ריק Stringלוקח 40 בתים - מספיק זיכרון שיתאים ל -20 תווי Java.

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

 אובייקט = "מחרוזת עם 20 תווים"; 

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

 public static String createString (final int length) { char [] result = new char [length]; for (int i = 0; i < length; ++ i) result [i] = (char) i; return new String (result); } 

After arming myself with this String creator method, I get the following results:

length: 0, {class java.lang.String} size = 40 bytes length: 1, {class java.lang.String} size = 40 bytes length: 2, {class java.lang.String} size = 40 bytes length: 3, {class java.lang.String} size = 48 bytes length: 4, {class java.lang.String} size = 48 bytes length: 5, {class java.lang.String} size = 48 bytes length: 6, {class java.lang.String} size = 48 bytes length: 7, {class java.lang.String} size = 56 bytes length: 8, {class java.lang.String} size = 56 bytes length: 9, {class java.lang.String} size = 56 bytes length: 10, {class java.lang.String} size = 56 bytes 

The results clearly show that a String's memory growth tracks its internal char array's growth. However, the String class adds another 24 bytes of overhead. For a nonempty String of size 10 characters or less, the added overhead cost relative to useful payload (2 bytes for each char plus 4 bytes for the length), ranges from 100 to 400 percent.

Of course, the penalty depends on your application's data distribution. Somehow I suspected that 10 characters represents the typical String length for a variety of applications. To get a concrete data point, I instrumented the SwingSet2 demo (by modifying the String class implementation directly) that came with JDK 1.3.x to track the lengths of the Strings it creates. After a few minutes playing with the demo, a data dump showed that about 180,000 Strings were instantiated. Sorting them into size buckets confirmed my expectations:

[0-10]: 96481 [10-20]: 27279 [20-30]: 31949 [30-40]: 7917 [40-50]: 7344 [50-60]: 3545 [60-70]: 1581 [70-80]: 1247 [80-90]: 874 ... 

That's right, more than 50 percent of all String lengths fell into the 0-10 bucket, the very hot spot of String class inefficiency!

במציאות, הם Stringיכולים לצרוך זיכרון עוד יותר מכפי שאורכיהם מציעים: שניהם Stringשנוצרו מתוך StringBuffers (באופן מפורש או באמצעות אופרטור השרשור '+') ככל הנראה יש charמערכים באורכים גדולים Stringמהאורכים המדווחים מכיוון StringBufferש בדרך כלל מתחילים בקיבולת של 16 ואז הכפל אותו append()בפעולות. כך, למשל, createString(1) + ' 'בסופו של דבר עם charמערך בגודל 16, ולא 2.

מה אנחנו עושים?

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

שיעורי עטיפה