מעקב אחר זיכרון מקומי ב- JVM

1. סקירה כללית

תהית אי פעם מדוע יישומי Java צורכים זיכרון הרבה יותר מהכמות שצוינה דרך הידועים -Xms ו -Xmx כוונון דגלים? מסיבות שונות ואופטימיזציות אפשריות, ה- JVM עשוי להקצות זיכרון מקורי נוסף. הקצאות נוספות אלה יכולות בסופו של דבר להעלות את הזיכרון הנצרך מעבר ל -Xmx הַגבָּלָה.

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

2. הקצאות ילידים

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

2.1. מטאספייס

על מנת לשמור על מטא-נתונים על המחלקות הטעונות, ה- JVM משתמש באזור ייעודי שאינו ערימה הנקרא מטאספייס. לפני Java 8 נקרא המקבילה פרמגן אוֹ דור קבוע. Metaspace או PermGen מכילים את המטא-נתונים על המחלקות הטעונות ולא על המקרים שלהם, שנשמרים בתוך הערימה.

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

  • -XX: MetaspaceSize ו -XX: MaxMetaspaceSize כדי להגדיר את גודל ה- Metaspace המינימלי והמקסימלי
  • לפני Java 8, -XX: PermSize ו -XX: MaxPermSize כדי להגדיר את גודל PermGen המינימלי והמרבי

2.2. חוטים

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

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

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

2.3. מטמון קוד

על מנת להריץ את ה- JVM bytecode בפלטפורמות שונות, יש להמיר אותו להוראות מכונה. מהדר JIT אחראי על אוסף זה בעת ביצוע התוכנית.

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

2.4. איסוף זבל

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

2.5. סמלים

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

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

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

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

2.6. מאגרי בתים מקומיים

ה- JVM הוא החשוד הרגיל למספר משמעותי של הקצאות מקומיות, אך לעיתים מפתחים יכולים להקצות ישירות גם זיכרון מקורי. הגישות הנפוצות ביותר הן malloc שיחה של ישיר JNI ו- NIO ByteBuffers.

2.7. דגלי כוונון נוספים

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

$ java -XX: + PrintFlagsFinal -version | grep 

ה PrintFlagsFinal מדפיס את כל -XX אפשרויות ב- JVM. לדוגמה, כדי למצוא את כל הדגלים הקשורים למטאספייס:

$ java -XX: + PrintFlagsFinal -version | grep Metaspace // קטוע uintx MaxMetaspaceSize = 18446744073709547520 {product} uintx MetaspaceSize = 21807104 {pd product} // קטום

3. מעקב אחר זיכרון מקומי (NMT)

כעת, כשאנו מכירים את המקורות הנפוצים להקצאת זיכרון מקורי ב- JVM, הגיע הזמן לברר כיצד לפקח עליהם. ראשית, עלינו לאפשר מעקב אחר זיכרון מקורי באמצעות דגל כוונון JVM נוסף: -XX: NativeMemoryTracking = off | סיכום | פרט. כברירת מחדל, ה- NMT כבוי אך אנו יכולים לאפשר לו לראות סיכום או תצוגה מפורטת של תצפיותיו.

נניח שברצוננו לעקוב אחר הקצאות מקוריות ליישום אופנתי של אתחול האביב:

$ java -XX: NativeMemoryTracking = סיכום -Xms300m -Xmx300m -XX: + UseG1GC -jar app.jar

כאן אנו מאפשרים את ה- NMT תוך הקצאת שטח של 300 מגה-בתים עם G1 כאלגוריתם GC שלנו.

3.1. תמונות מיידיות

כאשר NMT מופעל, אנו יכולים לקבל את פרטי הזיכרון המקורי בכל עת באמצעות ה- jcmd פקודה:

$ jcmd VM.native_memory

על מנת למצוא את ה- PID ליישום JVM, אנו יכולים להשתמש ב- jpsפקודה:

$ jps -l 7858 app.jar // זו האפליקציה 7899 sun.tools.jps.Jps שלנו

עכשיו אם נשתמש jcmd עם המתאים pid, ה VM.native_memory גורם ל- JVM להדפיס את המידע על הקצאות הילידים:

$ jcmd 7858 VM.native_memory

בואו ננתח את קטע הפלט של ה- NMT לפי פרק.

3.2. סך כל ההקצבות

NMT מדווחת על סך הזיכרון השמור והמחויב כדלקמן:

מעקב אחר זיכרון מקומי: סה"כ: שמור = 1731124KB, מחויב = 448152KB

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

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

לאחר הסעיף הכולל, NMT מדווחת על הקצאת זיכרון לכל מקור הקצאה. אז בואו נחקור כל מקור לעומק.

3.3. ערימה

NMT מדווחת על הקצאת הערמות שלנו כפי שציפינו:

ערימת Java (שמורה = 307200KB, מחויבת = 307200KB) (ממפה: שמורה = 307200KB, מחויבת = 307200KB)

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

3.4. מטאספייס

הנה מה שה- NMT אומר על מטא הנתונים של הכיתות עבור שיעורים טעונים:

מחלקה (שמורה = 1091407KB, מחויבת = 45815KB) (כיתות 6566) (malloc = 10063KB # 8519) (ממפה: שמורה = 1081344KB, מחויבת = 35752KB)

כמעט 1 GB שמור ו- 45 MB התחייבו לטעינת 6566 כיתות.

3.5. פְּתִיל

והנה דו"ח NMT על הקצאות חוטים:

חוט (שמור = 37018KB, מחויב = 37018KB) (חוט מס '37) (מחסנית: שמור = 36864KB, מחויב = 36864KB) (malloc = 112KB # 190) (זירה = 42KB # 72)

בסך הכל, 36 מגה בייט של זיכרון מוקצה לערימות עבור 37 שרשורים - כמעט 1 מגה בייט לכל מחסנית. JVM מקצה את הזיכרון לשרשורים בזמן היצירה, כך שההקצאות השמורות והמחויבות שוות.

3.6. מטמון קוד

בואו נראה מה NMT אומרת על הוראות ההרכבה שנוצרו ושמור במטמון על ידי JIT:

קוד (שמור = 251549KB, מחויב = 14169KB) (malloc = 1949KB # 3424) (mmap: שמור = 249600KB, מחויב = 12220KB)

נכון לעכשיו נשמרים במטמון כמעט 13 מגהבייט וסכום זה עשוי להגיע לכ- 245 מגהבייט.

3.7. GC

הנה דו"ח NMT אודות השימוש בזיכרון של G1 GC:

GC (שמור = 61771KB, מחויב = 61771KB) (malloc = 17603KB # 4501) (mmap: שמור = 44168KB, מחויב = 44168KB)

כפי שאנו רואים, כמעט 60 מגה בייט שמורים ומחויבים לעזור ל- G1.

בואו נראה איך נראה השימוש בזיכרון עבור GC פשוט בהרבה, נגיד GC סידורי:

$ java -XX: NativeMemoryTracking = סיכום -Xms300m -Xmx300m -XX: + UseSerialGC -jar app.jar

ה- GC הסידורי בקושי משתמש ב- 1 מגהבייט:

GC (שמור = 1034KB, מחויב = 1034KB) (malloc = 26KB # 158) (mmap: שמור = 1008KB, מחויב = 1008KB)

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

3.8. סֵמֶל

הנה דו"ח NMT על הקצאות הסמלים, כגון טבלת המיתרים והמאגר הקבוע:

סמל (שמור = 10148KB, מחויב = 10148KB) (malloc = 7295KB # 66194) (זירה = 2853KB # 1)

כמעט 10 מגה בייט מוקצה לסמלים.

3.9. NMT לאורך זמן

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

בסיס הבסיס של $ jcmd VM.native_memory הצליח

ואז, לאחר זמן מה, אנו יכולים להשוות את השימוש בזיכרון הנוכחי עם קו הבסיס:

$ jcmd VM.native_memory summary.diff

NMT, באמצעות סימני + ו- -, יגיד לנו כיצד השתנה השימוש בזיכרון באותה תקופה:

סה"כ: שמורה = 1771487KB + 3373KB, מחויבת = 491491KB + 6873KB - Java Heap (שמורה = 307200KB, מחויבת = 307200KB) (ממ"פ: שמורה = 307200KB, מחויבת = 307200KB) - מחלקה (שמורה = 1084300KB + 2103KB, מחויבת = 39356KB + 2871KB ) // קטום

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

3.10. NMT מפורט

NMT יכול לספק מידע מפורט מאוד על מפה של שטח הזיכרון כולו. כדי לאפשר דוח מפורט זה, עלינו להשתמש ב- -XX: NativeMemoryTracking = פרט כוונון דגל.

4. מסקנה

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


$config[zx-auto] not found$config[zx-overlay] not found