Java String Pool
כפי שידוע לנו, לא נהוג לבדוק ערך של String על ידי == אלא על ידי
()equals.
הסיבה היא שהשוואת == בודקת האם מדובר באותו
מיקום בזיכרון (אותו reference).
אבל בכל זאת הדוגמה הבאה תחזיר לנו true:
String s1 = "hello java";
String s2 = "hello java";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
אז מה בעצם קרה פה?
כמו ב-Integer Pool, גם String אינו מוזנח ב-Java. על מנת לחסוך בזיכרון ולנהל משאבים בצורה יעילה, Java שומרת String literals בתוך מאגר מיוחד הנקרא String Pool.
למידע נוסף על Integer Pool:
מה קורה כאשר משתמשים ב-new String?
אם ניצור String באמצעות new, ההשוואה באמצעות == לא תעבוד, מכיוון שהאובייקט החדש אינו משתמש ברפרנס שנמצא ב-String Pool.
String s3 = new String("hello java");
System.out.println(s2 == s3);
System.out.println(s2.equals(s3));
התוצאה תהיה:
- false עבור ==
- true עבור equals()
שימוש ב-()intern
אם נשתמש בפקודת ()intern, ההשוואה בשתי התצורות תחזיר true.
String s4 = s3.intern();
System.out.println(s2 == s4);
System.out.println(s2.equals(s4));
מה בעצם קרה כאן?
הפקודה ()intern ניגשת ל-String Pool ובודקת האם המחרוזת "hello java" כבר קיימת שם.
- אם היא קיימת - יוחזר הרפרנס הקיים מה-Pool.
- אם היא לא קיימת - היא תתווסף ל-Pool ויוחזר הרפרנס החדש.
שימוש לא נכון ב-()intern עלול להכביד על ה-String Pool ולפגוע בביצועים.
לדוגמה:
UUID.randomUUID().toString().intern();
הקוד הזה יכניס אלפי ערכים שונים ל-Pool ויגדיל אותו ללא צורך. מכיוון שכל UUID הוא ייחודי, אין סיבה אמיתית לשמור אותו ב-String Pool.
אז איך הזיכרון עובד?
ב-Java קיימים שני אזורי זיכרון עיקריים:
- Stack (מחסנית) - שומר משתנים מקומיים ורפרנסים לאובייקטים. מהיר מאוד לגישה.
- Heap (ערימה) - שומר את האובייקטים עצמם וכל מה שנוצר באמצעות new.
String Literal
String s1 = "hello java";
מה קורה בזיכרון?
- נוצר String ב-Heap.
- המחרוזת נשמרת גם ב-String Pool.
- המשתנה s1 ב-Stack מחזיק רפרנס לאובייקט.
└── s1 ───────────► "hello java"
String Pool
└── "hello java"
()new String
String s3 = new String("hello java");
כאן ייווצר אובייקט חדש לחלוטין ב-Heap.
└── s3 ───────────► Object #2
Heap
└── Object #2 = "hello java"
String Pool
└── "hello java"
Java 7 לעומת Java 8
התיאור למעלה מתאים ל-Java 8 ומעלה, אך בעבר המצב היה שונה. ב-Java 7 ומטה היה אזור מיוחד בשם PermGen (Permanent Generation).
PermGen היה חלק מהזיכרון של ה-JVM והכיל בעיקר metadata של מחלקות וכן מידע נוסף שהמערכת הגדירה כ"קבוע". עם השנים התברר שיש בעיה בגישה זו:
- ל-PermGen היה גודל קבוע.
- ה-Garbage Collector התקשה לנקות אותו.
- Frameworks מודרניים טוענים מחלקות בצורה דינמית.
- הדבר גרם לעיתים קרובות ל-OutOfMemoryError.
לכן Java 8 החליפה את PermGen ב-Metaspace.
Metaspace משתמש ב-Native Memory של מערכת ההפעלה ויכול לגדול בצורה דינמית בהתאם לצורך.
זה לא אומר שהזיכרון אינסופי - גם כאן ניתן להגיע למגבלה, אך המגבלה גדולה וגמישה בהרבה לעומת PermGen.
מבנה הזיכרון ב-Java 7
├── Heap
│ ├── Young
│ └── Old
└── PermGen (fixed)
מבנה הזיכרון ב-Java 8+
├── Heap
│ ├── Young
│ └── Old
└── Metaspace (native memory)