הבנת דליפות זיכרון בג'אווה

1. הקדמה

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

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

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

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

2. מהי דליפת זיכרון

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

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

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

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

תסמינים של דליפת זיכרון

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

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

3. סוגי דליפות זיכרון בג'אווה

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

3.1. דליפת זיכרון סטָטִי שדות

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

בג'אווה, סטָטִי לשדות יש חיים שתואמים בדרך כלל את כל חיי היישום הרץ (אֶלָא אִם ClassLoader הופך להיות זכאי לאיסוף אשפה).

בואו ליצור תוכנית Java פשוטה המאכלסת a סטָטִירשימה:

מחלקה ציבורית StaticTest {רשימת רשימה ציבורית סטטית = ArrayList חדש (); חלל ציבורי populateList () {for (int i = 0; i <10000000; i ++) {list.add (Math.random ()); } Log.info ("נקודת איתור באגים 2"); } ראשי ריק סטטי ציבורי (String [] args) {Log.info ("נקודת איתור באגים 1"); StaticTest חדש (). populateList (); Log.info ("נקודת איתור באגים 3"); }}

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

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

עם זאת, בתוכנית שלעיל, בשורה מספר 2, אם פשוט נפיל את מילת המפתח סטָטִי, אז זה יביא לשינוי דרסטי בשימוש בזיכרון, תגובת Visual Visual זו מראה:

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

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

כיצד למנוע זאת?

  • מזעור השימוש ב- סטָטִי משתנים
  • כשאתה משתמש בסינגלים, הסתמך על יישום שטוען בעצלתיים את האובייקט במקום לטעון בשקיקה

3.2. באמצעות משאבים לא סגורים

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

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

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

כיצד למנוע זאת?

  • השתמש תמיד סוף כל סוף חסום כדי לסגור משאבים
  • הקוד (אפילו ב סוף כל סוף בלוק) שסוגר את המשאבים לא אמור להיות לעצמו יוצאים מן הכלל
  • כאשר אנו משתמשים ב- Java 7+, אנו יכולים להשתמש ב- לְנַסוֹת-עם חסימת משאבים

3.3. לֹא מַתְאִים שווים() ו hashCode () יישומים

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

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

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

אדם בכיתה ציבורית {ציבור שם מחרוזת; אדם ציבורי (שם מחרוזת) {this.name = שם; }}

עכשיו נכניס כפילויות אדם חפצים לתוך א מַפָּה המשתמש במפתח זה.

זכרו כי א מַפָּה לא יכול להכיל מפתחות כפולים:

@Test ציבורי בטל givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Map map = HashMap חדש (); עבור (int i = 0; i <100; i ++) {map.put (אדם חדש ("jon"), 1); } Assert.assertFalse (map.size () == 1); }

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

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

למרות זאת, אם היינו עוקפים את שווים() ו hashCode () שיטות כמו שצריך, אז היה קיים רק אחת אדם התנגדו בזה מַפָּה.

בואו נסתכל על יישומים נכונים של שווים() ו hashCode () בשביל שלנו אדם מעמד:

אדם בכיתה ציבורית {ציבור שם מחרוזת; אדם ציבורי (שם מחרוזת) {this.name = שם; } @ עקוף בוליאני ציבורי שווה (אובייקט o) {אם (o == זה) יחזיר נכון; if (! (o instance of Person)) {return false; } אדם אדם = (אדם) o; להחזיר person.name.equals (שם); } @Override int hashCode ציבורי () {int result = 17; תוצאה = 31 * תוצאה + name.hashCode (); תוצאת החזרה; }}

ובמקרה זה, הטענות הבאות יהיו נכונות:

@Test public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak () {Map map = חדש HashMap (); עבור (int i = 0; i <2; i ++) {map.put (אדם חדש ("jon"), 1); } Assert.assertTrue (map.size () == 1); }

אחרי דריסה נכונה שווים() ו hashCode (), זיכרון הערמה של אותה תוכנית נראה כמו:

דוגמא נוספת היא לשימוש בכלי ORM כמו Hibernate, שמשתמש בו שווים() ו hashCode () שיטות לניתוח האובייקטים ושומר אותם במטמון.

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

כיצד למנוע זאת?

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

למידע נוסף, בקרו בהדרכות שלנו צור שווים() ו hashCode () עם ליקוי חמה ומדריך hashCode () בג'אווה.

3.4. שיעורים פנימיים המתייחסים לשיעורים חיצוניים

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

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

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

עם זאת, אם רק נכריז על המעמד הפנימי כסטטי, אז אותו מודל זיכרון נראה כך:

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

כיצד למנוע זאת?

  • אם הכיתה הפנימית אינה זקוקה לגישה לחברי הכיתה המכילים, שקול להפוך אותה ל- סטָטִי מעמד

3.5. דרך לְסַכֵּם() שיטות

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

בנוסף, אם הקוד כתוב לְסַכֵּם() השיטה אינה אופטימלית ואם תור המסיים לא יכול לעמוד בקצב האשפה של ג'אווה, במוקדם או במאוחר, היישום שלנו נועד לפגוש OutOfMemoryError.

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

עם זאת, אם רק נסיר את המעוקף לְסַכֵּם() שיטה, ואז אותה תוכנית נותנת את התגובה הבאה:

כיצד למנוע זאת?

  • עלינו להימנע תמיד מהגמר

לפרטים נוספים אודות לְסַכֵּם(), קרא את סעיף 3 (הימנעות מסופי גמר) במדריך שלנו לשיטת הגמר ב- Java.

3.6. התמחה מיתרים

הג'אווה חוּט הבריכה עברה שינוי משמעותי בג'אווה 7 כשהועברה מ- PermGen ל- HeapSpace. אך עבור יישומים הפועלים בגרסה 6 ומטה, עלינו להיות קשובים יותר בעבודה עם גדולים מיתרים.

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

PermGen למקרה זה ב- JVM 1.6 נראה כך ב- VisualVM:

בניגוד לזה, בשיטה, אם רק נקרא מחרוזת מקובץ ולא מתמחים בו, אז PermGen נראה כמו:

כיצד למנוע זאת?

  • הדרך הפשוטה ביותר לפתור בעיה זו היא על ידי שדרוג לגירסת Java העדכנית ביותר כאשר מאגר המיתרים מועבר ל- HeapSpace מגרסת Java 7 ואילך.
  • אם עובדים בגדול מיתריםהגדל את גודל שטח PermGen כדי למנוע פוטנציאל כלשהו OutOfMemoryErrors:
    -XX: MaxPermSize = 512 מטר

3.7. באמצעות ThreadLocalס

ThreadLocal (נדון בפירוט במבוא ל ThreadLocal במדריך Java) הוא מבנה הנותן לנו את היכולת לבודד מצב לשרשור מסוים ובכך מאפשר לנו להשיג בטיחות חוטים.

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

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

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

זיכרון דולף עם ThreadLocals

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

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

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

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

כיצד למנוע זאת?

  • זה נוהג טוב לנקות ThreadLocals כשהם כבר לא בשימוש - ThreadLocals לספק את לְהַסִיר() שיטה, המסירה את ערך השרשור הנוכחי עבור משתנה זה
  • אל תשתמש ThreadLocal.set (null) כדי לנקות את הערך - זה לא ממש מנקה את הערך אלא יחפש את מַפָּה המשויך לשרשור הנוכחי והגדר את צמד ערכי המפתח כחוט הנוכחי ו- ריק בהתאמה
  • עדיף אפילו לקחת בחשבון ThreadLocal כמשאב שצריך לסגור בא סוף כל סוף חסום רק כדי לוודא שהוא תמיד סגור, גם במקרה של חריג:
    נסה את {threadLocal.set (System.nanoTime ()); // ... עיבוד נוסף} סוף סוף {threadLocal.remove (); }

4. אסטרטגיות אחרות להתמודדות עם דליפות זיכרון

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

4.1. אפשר פרופיל

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

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

השתמשנו ב- Java VisualVM לאורך כל החלק 3 במדריך זה. אנא עיין במדריך שלנו לפרופילי Java כדי ללמוד על סוגים שונים של פרופילים, כמו Mission Control, JProfiler, YourKit, Java VisualVM ו- Netbeans Profiler.

4.2. אוסף זבל משוכלל

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

מילולית: gc

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

4.3. השתמש בחפצי עזר כדי להימנע מדליפות זיכרון

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

תורי עזר נועדו להודיע ​​לנו על פעולות שמבצע אספן האשפה. לקבלת מידע נוסף, קרא הפניות רכות במדריך Java Baeldung, במיוחד סעיף 4.

4.4. אזהרת דליפת זיכרון בליקוי

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

4.5. מידוד

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

לקבלת מידע נוסף אודות ביצועי ביצוע ביצועים, אנא עברו למדריכת Microbenchmarking עם Java שלנו.

4.6. ביקורות על קוד

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

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

5. מסקנה

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

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

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

כמו תמיד, קטעי הקוד המשמשים ליצירת תגובות VisualVM המתוארים במדריך זה זמינים ב- GitHub.


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