أساسيات جمع المهملات
أساسيات جمع المهملات
يُعدّ جامع المهملات (GC) في Java أحد السمات المميِّزة للـ JVM. فهو يُحرِّر تلقائيًا الذاكرة المشغولة بالكائنات التي لم يعد البرنامج قادرًا على الوصول إليها، مما يُلغي فئات بأكملها من الأخطاء — مؤشرات معلّقة، وتحرير مزدوج — التي يعانيها مطوّرو C و C++. إنّ فهم كيفية اتخاذ GC لقراراته أمرٌ لا غنى عنه لكتابة تطبيقات Java عالية الإنتاجية ومنخفضة الزمن الكامن.
قابلية الوصول: ما الذي يتتبّعه GC فعليًا؟
لا يتتبّع GC كل كائن على حدة، بل يعمل في اتجاه عكسي من مجموعة نقاط بداية محدَّدة تُسمّى جذور GC، ويجتاز كل مرجع تستطيع تلك الجذور الوصول إليه بصورة مباشرة أو غير مباشرة. أيّ كائن يمكن الوصول إليه من جذر GC يُعدّ حيًّا ولا يجوز جمعه. وأيّ كائن لا يمكن الوصول إليه من أيّ جذر يُعدّ غير مُتاح ومؤهَّلًا للجمع.
يتعرَّف JVM على عدة أنواع من جذور GC:
- المتغيرات المحلية والمعاملات على مكدّس استدعاء أي خيط نشط.
- الحقول الساكنة للفئات المحمَّلة — تبقى حيّةً طالما الفئة محمَّلة.
- الخيوط النشطة ذاتها (كائن
Threadيكون دائمًا جذرًا أثناء تشغيله). - مراجع JNI — الكائنات التي يحتفظ بها الكود الأصلي عبر JNI.
- أقفال المراقبة — الكائنات التي يحتفظ بها كتلة
synchronizedفي الوقت الحالي.
لاحظ المثال التالي:
جمع المهملات الجيلي
تتبّع كل كائن في كل دورة GC مكلفٌ جدًا على الأكوام الكبيرة. يستغلّ JVM ملاحظةً تجريبيةً قويةً تُسمّى الفرضية الجيلية:
معظم الكائنات تموت وهي صغيرة.
تُظهر دراسة تطبيقات Java الحقيقية باستمرار أن الغالبية العظمى من الكائنات — نتائج مؤقتة، بيانات نطاق الطلب، كائنات تكرارية — تصبح غير متاحة في غضون ميلي ثانية من إنشائها. في المقابل، تبقى أقلية ضئيلة من الكائنات (التخزين المؤقت، وتجمّعات الاتصال، والحالة على مستوى الفئة) طوال عمر التطبيق. يستغلّ GC الجيلي هذه الحقيقة بتقسيم الكومة إلى مناطق وجمع المنطقة الشابة بتكرار أعلى بكثير من المنطقة القديمة.
تخطيط الكومة
تنقسم كومة JVM إلى منطقتين رئيسيتين:
- الجيل الشاب (Eden + مساحتا Survivor S0 وS1): تُخصَّص هنا جميع الكائنات الجديدة. يعمل GC الشاب (المعروف بـ minor GC) بصورة متكررة ويجمع هذه المنطقة فقط — ولأن معظم كائناتها ميتة أصلًا، فهو سريع جدًا.
- الجيل القديم (مساحة Tenured): تُرقَّى الكائنات التي تنجو من عدد كافٍ من دورات minor GC إلى الجيل القديم. يعمل GC القديم (المعروف بـ major أو full GC) بصورة نادرة وهو أكثر تكلفةً بكثير.
الجيل الشاب بالتفصيل
يستخدم الجيل الشاب خوارزمية النسخ (semi-space):
- التخصيص — تذهب الكائنات الجديدة إلى Eden باستخدام مُخصِّص bump-pointer البسيط (سريع للغاية).
- إشغال minor GC — عندما يمتلئ Eden، يبدأ minor GC وتتوقف جميع الخيوط (إيقاف العالم).
- تتبّع الحيّة — يتتبّع GC من جذور GC ويُحدِّد الكائنات الحيّة في Eden ومساحة Survivor النشطة.
- النسخ — تُنسخ الكائنات الحيّة إلى مساحة Survivor الأخرى (الفارغة). الكائنات الميتة تُترك — لا توجد عملية تحرير لكل كائن على حدة.
- الترقية — تُرقَّى الكائنات التي نجت من عدد قابل للضبط من دورات GC (عتبة الاستمرارية، 15 افتراضيًا لمعظم الجامعين) إلى الجيل القديم.
- التبديل — تتبادل مساحتا Survivor الأدوار؛ المساحة التي أُفرِغت للتو تصبح النشطة الجديدة.
الترقية والجيل القديم
تصل الكائنات إلى الجيل القديم بطريقتين:
- تجاوزها عتبة الاستمرارية.
- امتلاء مساحة Survivor بما لا يتّسع لها (الترقية المبكرة).
الترقية المبكرة مشكلة أداء شائعة: إذا كان تطبيقك يُنشئ أعدادًا كبيرة من الكائنات متوسطة العمر، تمتلئ مساحات Survivor بسرعة، وتُرقَّى الكائنات مبكرًا، ويمتلئ الجيل القديم أسرع مما ينبغي — مما يُشغِّل دورات major GC مكلفة.
توقّف الجميع (Stop-the-World)
يستلزم معظم عمل GC إيقافًا شاملًا (STW): تتوقف جميع خيوط التطبيق بينما يعمل GC، ثم تُستأنَف. فترات توقف minor GC عادةً بضع ميلي ثانية. أما فترات توقف major GC فقد تصل إلى مئات الميلي ثانية أو أكثر على الأكوام الكبيرة مع الجامعين القدامى.
قوّة المراجع وقابلية الجمع
تمتلك Java أربع درجات لقوّة المراجع تؤثر في أهلية الجمع:
- مرجع قوي — مرجع Java عادي؛ الكائن لا يُجمَع أبدًا طالما هذا المرجع مُتاح.
SoftReference<T>— يُجمَع فقط حين يكون JVM على وشك رميOutOfMemoryError. مفيد للتخزين المؤقت الحساس للذاكرة.WeakReference<T>— يُجمَع في دورة GC التالية حين لا تبقى مراجع قوية أو ليّنة. يستخدمهWeakHashMap.PhantomReference<T>— يُوضَع في القائمة بعد الإنهاء؛ يُستخدم للتنظيف بعد الاستخدام (يُفضَّلCleanerفي Java الحديثة).
خلاصات عملية
يُمكِّنك فهم GC الجيلي وقابلية الوصول من التفكير في الأداء بدلًا من التخمين:
- الكائنات التي تموت قبل minor GC التالي تكون "مجانية" عمليًا — فهي لا تلمس الجيل القديم أبدًا.
- الكائنات طويلة العمر (المُفردات، التخزين المؤقت) ينبغي أن تكون طويلة العمر حقًا — تجنّب الأنماط التي تُرقِّي الكائنات ثم تُخلِّصها من الجيل القديم بصورة متكررة.
- المجموعات الساكنة هي جذور GC؛ أي شيء يُضاف إليها يبقى حيًّا حتى تُمسَح المجموعة أو يُزال الإدخال.
- استخدم
WeakReferenceأوSoftReferenceلحالات التخزين المؤقت التي لا ينبغي أن تمنع الجمع. - راقب مخرجات
-Xlog:gc*لرؤية التكرار الفعلي لـ minor/major GC وأوقات التوقف قبل ضبط أيّ أعلام.
الدرس التالي يمتدّ بهذا الأساس إلى خوارزميات GC المحددة المتاحة في JVM — Serial وParallel وG1 وZGC — ويوضح كيفية اختيار كل منها وضبطها وفق أنماط أعباء العمل المختلفة.