Stack, Heap & Metaspace

הזיכרון ב-Java מחולק למספר אזורים מרכזיים, כאשר שניים מהם מרכזיים לניהול משתנים ואובייקטים: ה-Stack (מחסנית) וה-Heap (ערמה).

ה-Stack (מחסנית)

ה-Stack הוא אזור זיכרון שמוקצה לכל Thread בנפרד. הוא פועל בשיטת LIFO (Last In, First Out - האחרון שנכנס הוא הראשון שיוצא).

מה נשמר בו?

תכונות מרכזיות:

2. ה-Heap (ערמה)

ה-Heap הוא אזור זיכרון מרכזי ומשותף שמשמש את כל התוכנית (לכל ה-Threads יש גישה אליו). הוא מיועד לאחסון דינמי של אובייקטים ומערכים.

מה נשמר בו?

ה־Heap ב־Java מחולק לאזורים שונים (Generations) בהתבסס על ההנחה המרכזית בניהול זיכרון שנקראת "ההיפותזה הדורית" (Weak Generational Hypothesis) והיא שרוב האובייקטים בתוכנית מתים צעירים מאוד (למשל, אובייקטים שנוצרו לצורך חישוב מקומי במתודה ומיד נזרקים).

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

ה־Heap מתחלק לשני אזורים מרכזיים:

1. Young Generation (הדור הצעיר)

זהו האזור שבו נוצרים כל האובייקטים החדשים ברגע שאנחנו כותבים new. האזור הזה מתמלא מהר מאוד, ולכן ה־GC מבקר בו בתדירות גבוהה מאוד (תהליך שנקרא Minor GC).

הדור הצעיר עצמו מחולק לשלושה תתי־אזורים:

2. Old Generation / Tenured Generation (הדור הישן)

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

מה נמצא בו? אובייקטים לטווח ארוך, כמו אובייקטי Singleton, קונפיגורציות גלובליות, קשרי מסדי נתונים (Connections), או מטמונים (Caches) שנשארים לאורך כל חיי האפליקציה.

איך ה־GC מנקה אותו? ניתן להעמיק בנושא זה כאן למידע נוסף על Garbage Collection (GC)

תכונות מרכזיות:

המחשה קצרה: Stack מול Heap בקוד

public void createPerson() {
    int age = 30; // 'age' (primitive) is stored directly on the Stack
    String name = "Alice"; // 'name' reference is on the Stack, the actual String object is in the Heap
}

3. איפה יושבים המטא-דאטה, המחלקות והקבועים? (האבולוציה בגרסאות Java)

הגדרות של מחלקות, ממשקים, מתודות, משתנים סטטיים (static) ומטא-דאטה לא יושבים ב-Heap הרגיל ולא ב-Stack, אלא באזור נפרד ששמו השתנה משמעותית בין גירסאות Java:

עד Java 7 (כולל) אזור ה-PermGen (Permanent Generation)

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

הבעיה: מכיוון שהגודל היה קבוע מראש, תוכניות גדולות או שימוש כבד בטעינת מחלקות דינמית (כמו שימוש ב-Frameworks כמו Spring או Hibernate, או יצירת Classes בזמן ריצה באמצעות Reflection) גרמו שוב ושוב לשגיאה המפורסמת: java.lang.OutOfMemoryError: PermGen space. היה קשה מאוד לנהל ולכוון את הגודל שלו נכון.

מ-Java 8 ועד היום מעבר ל-Metaspace

החל מגרסה Java 8, אזור ה-PermGen בוטל לחלוטין והוחלף ב-Metaspace.

השינוי המרכזי: ה-Metaspace אינו חלק עוד מזיכרון ה-JVM המוקצה, אלא הוא יושב ישירות על הזיכרון הפיזי של מערכת ההפעלה (Native Memory).

היתרון:

Java 7-

JVM Memory
├── Heap
│   ├── Young
│   └── Old
└── PermGen (fixed)

java 8+

JVM Memory
├── Heap
│   ├── Young
│   └── Old
└── Metaspace (native memory)
⚠️ אז ב JAVA8 ומעלה אין בעיה של זיכרון? הוא לא יכול להיגמר אף פעם?

Java 8 החליפה אותו ב־Metaspace שמשתמש ב־native memory ויכול לגדול דינמית, מה שהפך class loading והרצת frameworks להרבה יותר יציבים. אבל! הזיכרון נגזר ממערכת ההפעלה, כמה שהוא גדול, הוא עדיין מוגבל ולכן הוא כן יכול להיגמר.

🏠 Back to Orly's Code Corner