דגם זיכרון Java (JVM) – ניהול זיכרון ב-Java

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

דגם הזיכרון ב-Java (JVM)

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

ניהול הזיכרון ב-Java – הדור הצעיר

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

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

ניהול זיכרון ב-Java – דור הישן

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

עצר את האירוע של העולם

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

Java Memory Model – Permanent Generation

דור ההישרדות או "Perm Gen" מכיל את המטא-נתונים שנדרשים על ידי ה-JVM כדי לתאר את המחלקות והשיטות המשמשות ביישום. שים לב ש-Perm Gen אינו חלק מזיכרון ה- Java Heap. Perm Gen מתמלא על ידי ה-JVM בזמן הרצת היישום בהתבסס על המחלקות שמשמשות ביישום. ה-Perm Gen מכיל גם מחלקות ושיטות של ספריית Java SE. אובייקטים ב-Perm Gen נאספים באיסוף זבל מלא.

Java Memory Model – אזור השיטות

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

Java Memory Model – בריכת זיכרון

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

Java Memory Model – בריכת קבועים זמן ריצה

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

Java Memory Model – זיכרון תקן של ג'אווה

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

ניהול זיכרון בג'אווה – החלפות זיכרון של ה-Heap בג'אווה

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

VM Switch VM Switch Description
-Xms For setting the initial heap size when JVM starts
-Xmx For setting the maximum heap size.
-Xmn For setting the size of the Young Generation, rest of the space goes for Old Generation.
-XX:PermGen For setting the initial size of the Permanent Generation memory
-XX:MaxPermGen For setting the maximum size of Perm Gen
-XX:SurvivorRatio For providing ratio of Eden space and Survivor Space, for example if Young Generation size is 10m and VM switch is -XX:SurvivorRatio=2 then 5m will be reserved for Eden Space and 2.5m each for both the Survivor spaces. The default value is 8.
-XX:NewRatio For providing ratio of old/new generation sizes. The default value is 2.

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

ניהול זיכרון ב-Java – איסוף זבל ב-Java

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

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

ישנם שני בעיות עם גישת הסימון והמחיקה הפשוטה.

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

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

ניהול הזיכרון בג'אווה – סוגי איסוף הזבל בג'אווה

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

  1. Serial GC (-XX:+UseSerialGC): ה- Serial GC משתמש בגישת mark-sweep-compact הפשוטה לאיסוף הזבל של הדורות הצעירים והזקנים, כלומר Minor ו- Major GC. ה- Serial GC שימושי במכונות לקוח כמו היישומים הפשוטים שלנו ומכונות עם מעבד קטן יותר. זה טוב ליישומים קטנים עם רמת זיכרון נמוכה.
  2. Parallel GC (-XX:+UseParallelGC): קולקטור GC מקבוצה מקבוצה (Parallel GC) הוא זהה ל-GC סדרית (Serial GC) עם הבדל שהוא מייצר N תהליכים עבור איסוף זבל בדור צעיר, כאשר N הוא מספר הליבות במערכת. אנו יכולים לשלוט על מספר התהליכים באמצעות האפשרות -XX:ParallelGCThreads=n של מכונת ה-JVM. קולקטור זבלים מקבוצה (Parallel Garbage Collector) נקרא גם אוסף כתוצאה מהמהירות (throughput collector) מכיוון שהוא משתמש במרובעת המעבד לתיקוני GC. ה-GC המקבוצתי משתמש בתהליך יחיד עבור איסוף זבלים בדור הישן.
  3. Parallel Old GC (-XX:+UseParallelOldGC): זהה ל-GC מקבוצה (Parallel GC) עם הבדל שהוא משתמש במספר תהליכים לשני זרמים – לאיסוף זבלים בדור הצעיר וגם בדור הישן.
  4. Concurrent Mark Sweep (CMS) Collector (-XX:+UseConcMarkSweepGC): אוסף CMS נקרא גם אוסף תגובתי סט נמשך (concurrent low pause collector). הוא עובר על איסוף הזבלים עבור הדור הישן. אוסף CMS מנסה למזער את ההשהיות עקב איסוף זבלים על ידי ביצוע רוב העבודה של איסוף הזבלים באופן סמוי עם תהליכי היישום. האוסף CMS על הדור הצעיר משתמש באותו אלגוריתם כמו של אוסף מקבוצתי. קולקטור זבלים זה מתאים ליישומים תגובתיים בהם אין אפשרות להשהות זמן זרימה ארוך. אנו יכולים להגביל את מספר התהליכים בקולקטור CMS באמצעות האפשרות -XX:ParallelCMSThreads=n של מכונת ה-JVM.
  5. אוסף הזבל G1 (-XX:+UseG1GC): האוסף של Garbage First או G1 זמין מ-Java 7 ומטרתו העתידית היא להחליף את אוסף ה-CMS. אוסף ה-G1 הוא אוסף זבל מקבילי, תוך כדי, ומדחף באופן שובב תוך הפסקות קטנות. אוסף ה-Garbage First אינו פועל כמו אוספים אחרים ואין כאן מושג של מרחב צעיר וזקן. הוא מחלק את מרחב הזיכרון למספר אזורי זיכרון שווי גודל. כאשר נפעל איסוף אשפה, הוא קודם יאסוף את האזור עם נתונים חיים פחותים, לכן "Garbage First". ניתן למצוא פרטים נוספים על כך ב־מסמך הנחיות של Oracle לאוסף Garbage-First.

ניהול זיכרון ב-Java – מעקב אחר איסוף הזבל ב-Java

ניתן להשתמש בפקודת Java ממשק השורת פקודה וכלי ממשק משתמש גם בכלים לניטור פעילויות איסוף הזבל של אפליקציה. לדוגמה שלי, אני משתמש באפליקציית הדמו שסופקה על ידי הורדות של Java SE. אם ברצונך להשתמש באותה אפליקציה, עבור אל דף הורדות של Java SE והורד את JDK 7 ודמו גרפיים של JavaFX. האפליקציה לדוגמה שאני משתמש בה היא Java2Demo.jar והיא נמצאת בתיקייה jdk1.7.0_55/demo/jfc/Java2D. אך זהו שלב אופציונלי וניתן להפעיל את פקודות מעקב ה-GC עבור כל אפליקציה Java. הפקודה שהשתמשתי בה כדי להתחיל באפליקציית הדמו היא:

pankaj@Pankaj:~/Downloads/jdk1.7.0_55/demo/jfc/Java2D$ java -Xmx120m -Xms30m -Xmn10m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar Java2Demo.jar

jstat

ניתן להשתמש בכלי שורת הפקודה jstat כדי לנטר את זכרון ה-JVM ופעילויות איסוף הזבל. הוא מגיע עם JDK סטנדרטי, כך שלא תצטרך לעשות כלום נוסף כדי להשיג אותו. כדי להריץ את jstat עליך לדעת את זהות התהליך של האפליקציה, ניתן לקבל את זהות התהליך בקלות באמצעות הפקודה ps -eaf | grep java.

pankaj@Pankaj:~$ ps -eaf | grep Java2Demo.jar
  501 9582  11579   0  9:48PM ttys000    0:21.66 /usr/bin/java -Xmx120m -Xms30m -Xmn10m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseG1GC -jar Java2Demo.jar
  501 14073 14045   0  9:48PM ttys002    0:00.00 grep Java2Demo.jar

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

pankaj@Pankaj:~$ jstat -gc 9582 1000
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT
1024.0 1024.0  0.0    0.0    8192.0   7933.3   42108.0    23401.3   20480.0 19990.9    157    0.274  40      1.381    1.654
1024.0 1024.0  0.0    0.0    8192.0   8026.5   42108.0    23401.3   20480.0 19990.9    157    0.274  40      1.381    1.654
1024.0 1024.0  0.0    0.0    8192.0   8030.0   42108.0    23401.3   20480.0 19990.9    157    0.274  40      1.381    1.654
1024.0 1024.0  0.0    0.0    8192.0   8122.2   42108.0    23401.3   20480.0 19990.9    157    0.274  40      1.381    1.654
1024.0 1024.0  0.0    0.0    8192.0   8171.2   42108.0    23401.3   20480.0 19990.9    157    0.274  40      1.381    1.654
1024.0 1024.0  48.7   0.0    8192.0   106.7    42108.0    23401.3   20480.0 19990.9    158    0.275  40      1.381    1.656
1024.0 1024.0  48.7   0.0    8192.0   145.8    42108.0    23401.3   20480.0 19990.9    158    0.275  40      1.381    1.656

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

  • S0C ו-S1C: עמודה זו מציגה את גודל הנוכחי של אזורי ה-Survivor0 וה-Survivor1 בקילובייטים.
  • S0U ו-S1U: עמודה זו מציגה את השימוש הנוכחי באזורי ה-Survivor0 וה-Survivor1 בקילובייטים. שים לב שאחד מאזורי ה-Survivor ריק תמיד.
  • EC ו-EU: העמודות הללו מציגות את גודל המרווח הנוכחי ושימושו ב־Eden בקילובייטים. שים לב שגודל ה-EU מתרבה וכשהוא עובר את ה-EC, מתרץ GC מינימלי וגודל ה-EU יירד.
  • OC ו-OU: העמודות הללו מציגות את גודל הדור הישן הנוכחי ושימושו הנוכחי בקילובייטים.
  • PC ו-PU: העמודות הללו מציגות את גודל הדור Perm הנוכחי ושימושו הנוכחי בקילובייטים.
  • YGC ו-YGCT: עמודת YGC מציגה את מספר אירועי GC שקרו בדור הצעיר. עמודת YGCT מציגה את הזמן המצטבר לפעולות GC עבור דור הצעיר. שים לב ששתי העמודות מתרבות באותה שורה בה ערך ה-EU יורד בשל GC מינימלי.
  • FGC ו-FGCT: עמודת FGC מציגה את מספר אירועי GC מלא שקרו. עמודת FGCT מציגה את הזמן המצטבר לפעולות GC מלא. שים לב שהזמן של GC מלא גבוה מדי בהשוואה לזמני GC של דור הצעיר.
  • GCT: עמודה זו מציגה את הזמן המצטבר הכולל לפעולות GC. שים לב שזהו סכום של ערכי העמודות YGCT ו-FGCT.

היתרון של jstat הוא שניתן להפעיל אותו גם בשרתים מרוחקים שבהם אין לנו ממשק משתמש גרפי. שים לב שסכום של S0C, S1C ו-EC הוא 10 מגה כפי שצוין באמצעות אפשרות ה-JVM -Xmn10m.

Java VisualVM עם Visual GC

אם ברצונך לראות פעולות זיכרון וניהול אשפה בממשק משתמש גרפי (GUI), תוכל להשתמש בכלי jvisualvm. Java VisualVM היא גם חלק מ-JDK, כך שאין צורך להוריד אותו בנפרד. פשוט הפעל את הפקודה jvisualvm בטרמינל כדי להפעיל את יישום Java VisualVM. לאחר ההפעלה, עליך להתקין את התוסף Visual GC מתוך האפשרות Tools – Plugins, כפי שמוצג בתמונה למטה. לאחר התקנת Visual GC, פשוט פתח את היישום מהעמודה השמאלית ועבור לקטע Visual GC. תקבל תמונה של זיכרון JVM ופרטי איסוף אשפה כפי שמוצג בתמונה למטה.

כיוון איסוף אשפה של Java

כיוון אוסף הזבל של Java צריך להיות האפשרות האחרונה שתשתמשו בה כדי להגביר את תפוקת היישום שלך, ורק כאשר אתה רואה ירידה בביצועים בשל זמני GC ארוכים שגורמים לזמני היישום להתמוטט. אם אתה רואה שגיאות java.lang.OutOfMemoryError: PermGen space בלוגים, אז נסה לעקוב ולהגדיל את מרווח הזיכרון של Perm Gen באמצעות אפשרויות ה- JVM -XX:PermGen ו-XX:MaxPermGen. כדאי גם לנסות להשתמש ב־-XX:+CMSClassUnloadingEnabled ולבדוק איך זה פועל עם אוסף הזבל של CMS. אם אתה רואה הרבה פעולות Full GC, אז כדאי לנסות להגדיל את מרווח הזיכרון של הדור הישן. בכלל, כיוון אוסף הזבל מחייב המון מאמץ וזמן ואין כללים קשוחים ומהירים לכך. תצטרך לנסות אפשרויות שונות ולהשוות אותם כדי למצוא את האחת הטובה ביותר שמתאימה ליישום שלך. זהו הכל לדגם הזיכרון של Java, ניהול הזיכרון ב-Java ואוסף הזבל, אני מקווה שזה יעזור לך להבין את זיכרון ה-JVM ואת תהליך אוסף הזבל.

Source:
https://www.digitalocean.com/community/tutorials/java-jvm-memory-model-memory-management-in-java