نموذج ذاكرة Java
نموذج ذاكرة Java
كل بايت يخصّصه برنامجك في Java يقع في مكان محدد — في الـ Heap، أو على الـ Stack، أو في Metaspace. معرفة أين بالضبط تعيش كل فئة من البيانات، ولماذا تعيش هناك، وماذا يحدث حين تمتلئ تلك المناطق، هي معرفة أساسية لكتابة Java عالية الأداء منخفضة الضغط على جامع المهملات. هذا الدرس يمنحك تلك الخريطة.
الصورة الكاملة: مناطق البيانات أثناء التشغيل
تُعرِّف مواصفات JVM عدة مناطق بيانات أثناء التشغيل (runtime data areas). من منظور الأداء اليومي، ثلاثة منها تهم أكثر من غيرها:
- الـ Heap — حيث تعيش جميع نسخ الكائنات والمصفوفات.
- الـ JVM Stack (واحد لكل خيط تنفيذ) — حيث تعيش إطارات التوابع والمتغيرات المحلية ومكدسات المعاملات.
- Metaspace (استبدل PermGen في Java 8) — حيث تعيش بيانات وصف الفئات (class metadata).
تُكمل منطقتان أصغر الصورة: سجل PC (عداد البرنامج لكل خيط) ومكدسات التوابع الأصلية (Native Method Stacks) لكود JNI. نادرًا ما يُضبَط هاتان المنطقتان مباشرة.
الـ Heap
الـ Heap هو المخصّص المركزي لجميع كائنات Java. حين تكتب new ArrayList<>()، يستقر الكائن الناتج على الـ Heap — ويبقى هناك حتى يُثبت جامع المهملات أنه غير قابل للوصول.
يُقسّم JVM داخل الـ Heap المساحة إلى أجيال (لجامعي مهملات كـ G1 وParallel GC) أو مناطق (regions) (G1) / شرائح (segments) (ZGC، Shenandoah). النموذج الكلاسيكي الذي لا يزال يُنظَّم به التفكير يتضمن ثلاث مناطق:
- الجيل الشاب (Young Generation: Eden + فضاءا Survivor) — حيث يُخصَّص كل كائن جديد في البداية. تعمل Minor GC هنا بتكرار؛ معظم الكائنات تموت شابة (وهو ما يُعرف بـ"فرضية الأجيال").
- الجيل القديم (Old Generation / Tenured) — تُرقَّى إليه الكائنات التي تنجو من دورات كافية من Minor GC. تستعيد منه Major GC (أو Full GC) المساحة.
أعلام الـ Heap الأساسية (مثال مع G1):
تخطيط الكائن في الـ Heap
فهم كيفية تخطيط الكائنات في الذاكرة يُفسّر لماذا تُخصّص بعض الأنماط أكثر مما يبدو. كل كائن على الـ Heap يحتوي على رأس (header) (12 بايت عادةً عند تفعيل OOPs المضغوطة، أو 16 بايت بدونها) يليه حقول النسخة، مع حشو للمحاذاة. هذا يعني أن فئة بحقل byte واحد تشغل 16 بايت على الأقل.
أداة JOL (Java Object Layout) تطبع التخطيط الدقيق أثناء التشغيل:
هذا مهم عند تصميم هياكل بيانات صديقة للذاكرة المؤقتة، أو عند تتبع انتفاخ ظاهري في الـ Heap ناجم عن تكلفة الرأس في ملايين الكائنات الصغيرة.
الـ JVM Stack
كل خيط تنفيذ يمتلك JVM Stack خاصًا به، يُنشأ عند بدء الخيط ويُتلف عند انتهائه. المكدس مؤلف من إطارات (frames): يُدفع إطار لكل استدعاء تابع ويُسحب عند عودة ذلك التابع أو إلقاء استثناء.
يحتوي الإطار على:
- مصفوفة المتغيرات المحلية — تحمل معاملات التابع والمتغيرات المُعلَنة. الأنواع الأولية (
int،long،doubleإلخ) تُخزَّن مباشرةً كقيم هنا. - مكدس المعاملات (Operand Stack) — مساحة العمل لعمليات البايت-كود.
- مرجع إلى مجمع الثوابت أثناء التشغيل — روابط رمزية تُحلّ إلى عناوين ذاكرة ملموسة.
الانعكاس الحاسم: الأنواع الأولية في المتغيرات المحلية لا تلمس الـ Heap. المراجع (references) فقط تُخزَّن في مصفوفة المتغيرات المحلية — الكائن الذي تشير إليه يعيش على الـ Heap، لكن المرجع ذاته (مؤشر 4 أو 8 بايت) يجلس على المكدس.
حجم المكدس يُتحكّم به لكل خيط بـ -Xss (الافتراضي عادةً 512 كيلوبايت – 1 ميغابايت). التعاود العميق يُنمّي المكدس حتى يُلقى StackOverflowError. كثرة الخيوط تعني مزيدًا من إجمالي ذاكرة المكدس حتى لو كانت الخيوط خاملة.
Metaspace
قبل Java 8، كانت بيانات وصف الفئات تُخزَّن في الجيل الدائم (PermGen) — منطقة ذات حجم ثابت داخل الـ Heap. كانت مصدرًا شهيرًا لـ OutOfMemoryError: PermGen space في التطبيقات التي تُحمّل فئات كثيرة. استبدلتها Java 8 بـ Metaspace.
تُخزّن Metaspace:
- هياكل الفئات (واصفات الحقول، واصفات التوابع، vtable)
- بايت-كود التوابع ومؤشرات الكود الأصلي المُجمَّع بـ JIT
- مدخلات مجمع الثوابت أثناء التشغيل
- التعليقات التوضيحية (Annotations)
الفرق الجوهري عن PermGen: تعيش Metaspace في الذاكرة الأصلية (native memory) خارج الـ Heap وتنمو تلقائيًا بالافتراضي. يُلغي هذا مشكلة الحجم الثابت القديمة، لكنه يُدخل مشكلة جديدة: تطبيق يعاني من تسرب في ClassLoader سيستهلك الذاكرة الأصلية بصمت حتى ينفد مخزون نظام التشغيل.
تُستعاد Metaspace حين يصبح ClassLoader الفئة نفسه غير قابل للوصول. في تطبيق Spring Boot العادي نادرًا ما يحدث هذا (ClassLoader واحد لعمر العملية). في OSGi وJakarta EE مع النشر الحار والأطر التي تُولّد فئات وكيلة (Hibernate، ByteBuddy)، تسربات ClassLoader حقيقية وستستنفد Metaspace مع الوقت.
مجمع السلاسل النصية ومجمع الثوابت
السلاسل النصية الحرفية تُضاف إلى مجمع السلاسل (String pool) — جدول تديره JVM لكائنات String غير مكررة مُخزَّنة على الـ Heap (منذ Java 7). حرفيان متطابقان يشتركان في نفس كائن الـ Heap:
استخدام new String(…) صريحًا خطأ في كل الأحوال تقريبًا. يتجاوز المجمع ويهدر الـ Heap، وهو السبب الحقيقي لقاعدة "قارن السلاسل بـ equals() لا بـ ==".
تحليل الهروب والتخصيص على المكدس
يُجري مُجمِّع JIT تحليل الهروب (escape analysis): إن أثبت أن كائنًا لا "يهرب" من التابع المنشئ (لا يُخزَّن في حقل، ولا يُمرَّر لخيط آخر، ولا يُعاد)، قد يُخصّصه على المكدس أو يُلغي التخصيص كليًا (scalar replacement).
لهذا السبب، حلقات داخلية مُحكمة تُنشئ كائنات مساعدة صغيرة كثيرًا ما تؤدي أفضل مما يُوحي التحليل الساذج — قد يُزيل JIT التخصيص كليًا. القياس بـ JMH (درس 6) هو الطريقة الموثوقة الوحيدة للتحقق من هذا السلوك.
استراتيجية عملية لتحجيم الذاكرة
- القياس أولًا — استخدم
jmap -histoأو VisualVM أو async-profiler لتحديد ما هو موجود فعلًا على الـ Heap قبل ضبط الأعلام. - حجّم الـ Heap انطلاقًا من مجموعة البيانات الحية — قِس المجموعة الحية بعد Full GC؛ استهدف 2–3 أضعافها كحد أقصى للـ Heap ليتنفس جامع المهملات.
- ضع سقفًا لـ Metaspace — اضبط دائمًا
-XX:MaxMetaspaceSizeفي الإنتاج لاكتشاف تسربات ClassLoader مبكرًا. - احسب الذاكرة الأصلية — ذاكرة المكدس + Metaspace + المخازن المؤقتة المباشرة + كاش كود JIT كلها تعيش خارج الـ Heap. RSS الكلي للـ JVM = Heap + أصلية. لا تدع قاتل OOM في الحاوية يكتشف هذا على حسابك.
/proc/meminfo للذاكرة الكلية للجهاز ويضبط -Xmx افتراضيًا على 25% منها — متجاهلًا حدود ذاكرة الحاوية. Java 10+ تُراعي حدود cgroup عبر -XX:+UseContainerSupport (مُفعَّلة افتراضيًا منذ Java 11). تحقق دائمًا أن JVM في حاويتك يقرأ الحد الصحيح بـ java -XX:+PrintFlagsFinal -version | grep MaxHeapSize.
الخلاصة
الـ Heap هو موطن جميع نسخ الكائنات؛ هيكله الجيلي يُتيح جمعًا فعّالًا للمهملات. مكدس الخيط يحمل الإطارات والأنواع الأولية والمراجع — دون لمس الـ Heap لأنواع القيم. Metaspace استبدلت PermGen وتُخزّن بيانات وصف الفئات في الذاكرة الأصلية التي يجب تحديد سقف لها لاكتشاف التسربات. فهم أين تعيش البيانات يُمكّنك من التنبؤ بضغط التخصيص، وتحجيم مناطق الذاكرة بشكل صحيح، وتفسير سجلات جامع المهملات بذكاء — مهارات ستطبّقها طوال بقية هذا البرنامج التعليمي.