جمع القمامة في جافا: ما هو GC وكيف يعمل داخل JVM؟
مقدمة إلى جمع القمامة في جافا
يُعد Garbage Collection أو جمع القمامة أحد أهم المكونات التي تمنح لغة Java قدرتها على إدارة الذاكرة تلقائياً بكفاءة. فعند تشغيل التطبيق داخل JVM، تُنشأ الكائنات في مساحة الذاكرة المعروفة باسم Heap، ومع مرور الوقت يصبح بعض هذه الكائنات غير مستخدم. هنا يتدخل جامع القمامة لتحرير الذاكرة وإعادة استخدامها.
تكمن أهمية هذه الآلية في أنها تخفف العبء عن المطور، فلا يحتاج إلى تحرير الذاكرة يدوياً كما في لغات مثل C وC++. وهذا يقلل من احتمالات الوقوع في مشكلات مثل memory leaks أو أخطاء OutOfMemoryError.

ما هو جمع القمامة في جافا؟
جمع القمامة هو عملية استرجاع الذاكرة غير المستخدمة أثناء وقت التشغيل عبر إزالة الكائنات التي لم يعد هناك أي مرجع فعّال إليها. بعبارة أبسط، إذا أنشأ التطبيق كائناً ثم توقف عن استخدامه، فإن GC يعتبره مرشحاً للتحرير.
في التطبيقات المكتوبة بلغة Java، تتحول الشيفرة إلى bytecode وتُنفَّذ داخل Java Virtual Machine. ومع كل عملية إنشاء كائن جديد، يتم استهلاك جزء من Heap. وإذا لم تُحرَّر الذاكرة غير المستخدمة بانتظام، فقد تنخفض كفاءة التطبيق تدريجياً.
يمكن تقسيم الكائنات في الذاكرة إلى نوعين:
- كائنات حيّة: ما زالت هناك مراجع تشير إليها، وبالتالي ما زال التطبيق يستخدمها.
- كائنات ميتة: لا توجد أي مراجع تصل إليها، لذلك يمكن التخلص منها بأمان.
لماذا تحتاج جافا إلى جمع القمامة؟
في اللغات التي تعتمد على الإدارة اليدوية للذاكرة مثل C وC++، يتحمل المطور مسؤولية إنشاء الكائنات وتحريرها باستخدام دوال مثل free() أو delete(). وإذا نسي المطور إزالة كائن لم يعد مستخدماً، تبقى الذاكرة محجوزة بلا فائدة، وهو ما يؤدي إلى تسرب الذاكرة.
أما في Java، فإن هذه المهمة تتم بشكل آلي، ما يحقق عدة مزايا:
- تقليل احتمالات تسرب الذاكرة.
- تبسيط الشيفرة البرمجية.
- رفع إنتاجية المطور.
- تحسين استقرار التطبيقات طويلة التشغيل.
كيف تجعل الكائن مرشحاً لجمع القمامة؟
الهدف الأساسي من GC هو تحرير مساحة Heap من الكائنات التي لم تعد تملك مراجع فعالة. وعندما يفقد الكائن جميع مراجعه، يصبح من الممكن التخلص منه.
1) جعل المرجع يساوي null
Student student = new Student();
student = null;
2) إسناد المرجع إلى كائن آخر
Student studentOne = new Student();
Student studentTwo = new Student();
studentOne = studentTwo; // now the first object referred by studentOne is available for garbage collection
3) استخدام كائن مجهول
register(new Student());
في هذه الحالات، قد يصبح الكائن غير قابل للوصول، وعندها يمكن لجامع القمامة معالجته في دورة لاحقة.
كيف يعمل جمع القمامة داخل JVM؟
عملية جمع القمامة في Java تلقائية بالكامل، ولا يُطلب من المطور تعليم الكائنات المطلوب حذفها صراحةً. تتولى JVM هذه المهمة اعتماداً على خوارزميات داخلية تتبع المراجع وتحدد ما إذا كان الكائن لا يزال قابلاً للوصول أم لا.
ورغم أن كل تنفيذ لـ JVM قد يختلف في التفاصيل، فإن الفكرة العامة ثابتة:
- تحديد الكائنات القابلة للوصول.
- تمييز الكائنات غير القابلة للوصول.
- تحرير الذاكرة الخاصة بالكائنات غير المستخدمة.
- إعادة ترتيب الذاكرة أحياناً لتقليل التجزئة.
ما هي جذور جمع القمامة GC Roots؟
يعتمد جامع القمامة على مفهوم GC Roots لتحديد الكائنات الحيّة. يبدأ الفحص من هذه الجذور، ثم يتتبع جميع المراجع المتفرعة منها داخل الرسم البياني للكائنات في الذاكرة.
من أمثلة GC Roots:
- الفئات المحملة بواسطة محمّل النظام.
- الخيوط الحية
Live Threads. - المتغيرات المحلية ومعاملات الدوال الجاري تنفيذها.
- مراجع
JNIالمحلية والعالمية. - الكائنات المستخدمة في التزامن
synchronizationكمراقبات. - الكائنات التي تحتفظ بها
JVMلأغراض داخلية.

مراحل جمع القمامة في جافا
غالباً ما تمر عملية Garbage Collection بثلاث مراحل رئيسية.
1) تحديد الكائنات الحيّة Mark
في هذه المرحلة، يتتبع الجامع الرسم البياني للكائنات بداية من GC Roots. وكل كائن يمكن الوصول إليه يتم تمييزه على أنه حي.
أما الكائنات التي لا يمكن الوصول إليها، فتُعد نفايات قابلة للحذف.

2) إزالة الكائنات الميتة Sweep
بعد انتهاء التعليم، يعرف النظام أي أجزاء الذاكرة تخص كائنات حية وأيها تخص كائنات ميتة. في مرحلة Sweep، تُحرَّر المقاطع المرتبطة بالكائنات غير المستخدمة.

3) ضغط الذاكرة Compact
بعد حذف الكائنات الميتة، قد تبقى الذاكرة مجزأة، أي أن المساحات الفارغة تكون متناثرة. لذلك قد تقوم بعض أنواع المجمعات بضغط الكائنات الحية إلى بداية Heap لتصبح الذاكرة أكثر انتظاماً وأسهل في تخصيص كائنات جديدة.

ما هو جمع القمامة الجيلي Generational GC؟
تعتمد Java على استراتيجية ذكية تُسمى Generational Garbage Collection، وهي تقوم على فكرة أن معظم الكائنات تعيش لفترة قصيرة. لذلك لا معنى لفحص كل الكائنات بالطريقة نفسها في كل دورة.
بناءً على ذلك، تُقسم الذاكرة إلى أجيال حسب عمر الكائنات، ما يساعد على تحسين الأداء وتقليل زمن التوقف.

تقسيم Heap في جافا

الجيل الشاب Young Generation
تبدأ الكائنات الجديدة حياتها في هذا الجزء. وينقسم عادة إلى:
Eden: تُنشأ فيه الكائنات الجديدة لأول مرة.Survivor Spaces: وهماFromSpaceوToSpace، ويُنقل إليهما ما ينجو من دورات الجمع.
عند امتلاء Eden، تحدث عملية Minor GC. تُحذف الكائنات الميتة، وتُنقل الكائنات الحية إلى إحدى مساحتي Survivor.
ومن المهم فهم أن إحدى مساحتي Survivor تبقى فارغة دائماً تقريباً أثناء التبديل بينهما.
يمكن ضبط حجم Young Generation باستخدام الوسيط -Xmn.
كيف تنتقل الكائنات داخل الجيل الشاب؟
- يُنشأ الكائن أولاً في
Eden. - عند حدوث
Minor GC، تنتقل الكائنات الحية إلىFromSpace. - في الدورة التالية، تنتقل الكائنات الحية من
EdenوFromSpaceإلىToSpace. - بعد تكرار النجاة لعدد معين من الدورات، يُرقّى الكائن إلى الجيل القديم.
الجيل القديم Old Generation
يحتوي هذا الجزء على الكائنات التي عاشت فترة أطول ونجت من عدة دورات داخل الجيل الشاب. ويُعرف أيضاً باسم Tenured Generation.
عندما تُجمع النفايات من هذا الجزء، تسمى العملية Major GC. وعادة ما تكون أكثر كلفة من Minor GC بسبب حجم البيانات الأكبر.
يمكن ضبط أحجام Heap الابتدائية والقصوى باستخدام -Xms و-Xmx.
الجيل الدائم Permanent Generation
في الإصدارات الأقدم من Java، كانت البيانات الوصفية مثل تعريفات الأصناف والطرائق تُخزن في PermGen. وكان بالإمكان التحكم بحجمها باستخدام -XX:PermGen و-XX:MaxPermGen.
Metaspace في Java 8 وما بعده
ابتداءً من Java 8، تم استبدال PermGen بـ Metaspace. هذا التغيير ساعد على تقليل مشكلات نفاد الذاكرة الناتجة عن الحدود الصارمة للمساحة القديمة، لأن Metaspace يمكنها التوسع تلقائياً بحسب الحاجة.
أنواع جامعات القمامة في JVM
تضم JVM عدة أنواع من Garbage Collectors، ولكل نوع خصائص تناسب سيناريوهات معينة.
Serial GC
أبسط أنواع جمع القمامة، ويعمل بخيط واحد فقط. يناسب التطبيقات الصغيرة والبيئات أحادية المعالج أو قليلة التعقيد.
عيبه الأساسي أنه يسبب حدث stop the world، أي إيقاف التطبيق بالكامل أثناء الجمع.
وسيط التفعيل:
-XX:+UseSerialGC

Parallel GC
مناسب للتطبيقات متوسطة وكبيرة الحجم، خاصة على العتاد متعدد الأنوية. يستخدم عدة خيوط لجمع الجيل الشاب، ويُعرف أيضاً باسم Throughput Collector.
يعطي أولوية للإنتاجية العالية، لكنه قد يسبب توقفات ملحوظة.
وسيط التفعيل:
-XX:+UseParallelGC

Parallel Old GC
هو تطوير لـ Parallel GC بحيث يستخدم عدة خيوط أيضاً في جمع الجيل القديم، لا الجيل الشاب فقط. أصبح هذا السلوك هو الافتراضي في نسخ حديثة من Java.
وسيط التفعيل:
-XX:+UseParallelOldGC
CMS GC
الاسم الكامل هو Concurrent Mark Sweep. يركز على تقليل فترات التوقف عبر تنفيذ جزء كبير من العمل بالتوازي مع التطبيق.
من مزاياه أنه مناسب للتطبيقات الحساسة لزمن الاستجابة، لكنه يستهلك CPU أكثر من بعض الأنواع الأخرى، كما أنه لا يجري compaction بشكل افتراضي.
وسيط التفعيل:
-XX:+UseConcMarkSweepGC

G1 GC
يُعد G1 أو Garbage First من أشهر المجمعات الحديثة، وقد صُمم ليكون بديلاً مناسباً لـ CMS، خصوصاً مع الذاكرة الكبيرة التي تتجاوز 4GB.
بدلاً من تقسيم Heap إلى مساحات متجاورة تقليدية، يقسمها إلى مناطق Regions متساوية الحجم. ثم يحدد المناطق الأكثر احتواءً على نفايات ليجمعها أولاً.
من خصائصه:
- يعمل بشكل متوازٍ ومتزامن.
- يسمح بضبط أفضل لأزمنة التوقف.
- يتعامل بمرونة مع الأجيال داخل مناطق منفصلة.
كما يتضمن نوعين إضافيين من المناطق:
Humongous: للكائنات الضخمة.Available: للمساحة غير المخصصة.
وسيط التفعيل:
-XX:+UseG1GC

Epsilon GC
جامع تجريبي ظهر مع JDK 11، وهو في الحقيقة لا يجمع القمامة إطلاقاً. يقوم فقط بتخصيص الذاكرة، وعند امتلاء Heap يتوقف التطبيق.
قد يكون مفيداً في اختبارات الأداء المتخصصة أو في التطبيقات التي يعرف مطوروها استهلاك الذاكرة بدقة شديدة.
وسيط التفعيل:
-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
Shenandoah GC
مُطلق مع JDK 12، ويركز على تقليل التوقفات عبر تنفيذ مزيد من خطوات الجمع بالتزامن مع خيوط التطبيق، بما في ذلك نقل الكائنات أثناء التشغيل.
هذه الميزة تمنحه زمناً أقل للتوقف، لكنها قد تزيد استهلاك CPU.
وسيط التفعيل:
-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC
ZGC
ظهر مع JDK 11 وتطور في JDK 12. صُمم خصيصاً للتطبيقات التي تحتاج زمناً منخفضاً جداً للتوقف، غالباً أقل من 10 ms، أو التي تعمل مع Heap ضخمة جداً.
من أبرز مزاياه:
- زمن توقف شديد الانخفاض.
- قابلية توسع ممتازة.
- إعادة الذاكرة غير المستخدمة إلى نظام التشغيل تلقائياً.
في كثير من الحالات، تبقى فترات التوقف ضمن حدود تقارب 2ms.
وسيط التفعيل:
-XX:+UnlockExperimentalVMOptions -XX:+UseZGC

ملاحظة: أُضيف كل من Shenandoah وZGC تدريجياً إلى مسار الاستخدام الإنتاجي في الإصدارات الأحدث من JDK.
كيف تختار جامع القمامة المناسب؟
اختيار الجامع المناسب لا يعتمد على الشهرة، بل على طبيعة التطبيق ومتطلباته التشغيلية. في كثير من الأحيان، تكفي الإعدادات الافتراضية في JVM وتقدم نتائج جيدة دون تدخل يدوي.
لكن إذا احتجت إلى تخصيص أدق، فيمكن الاسترشاد بالتالي:
Serial: للتطبيقات الصغيرة جداً أو البيئات محدودة الموارد.Parallel: عندما تكون الأولوية للإنتاجية العالية ويكون التوقف الطويل مقبولاً.CMSأوG1: عندما يكون زمن الاستجابة أهم من أقصى إنتاجية.ZGC: عندما تكون أولوية قصوى لتقليل التوقف مع ذاكرة ضخمة.
مزايا جمع القمامة في جافا
- تبسيط إدارة الذاكرة للمطور.
- تقليل الأخطاء الناتجة عن التحرير اليدوي الخاطئ.
- تقليص مخاطر تسرب الذاكرة.
- تحسين استخدام
Heapعبر استرجاع المساحات غير المستخدمة. - توفير خيارات متعددة للضبط والتحسين حسب نوع التطبيق.
أفضل الممارسات عند التعامل مع GC
تجنب التحفيز اليدوي قدر الإمكان
يمكنك استدعاء System.gc() أو Runtime.gc()، لكن هذه الأوامر لا تضمن أن يبدأ الجمع فعلاً. كما أن استخدامها العشوائي قد يضر بالأداء بدلاً من تحسينه.
استخدم أدوات المراقبة والتحليل
إذا لاحظت بطئاً، أو توقفات طويلة، أو أخطاء نفاد الذاكرة، فاستعن بأدوات مثل jstat وJava Flight Recorder لتحليل سلوك الذاكرة واكتشاف النمو غير الطبيعي في الاستهلاك.
ابدأ بالإعدادات الافتراضية
في أغلب التطبيقات الصغيرة أو المتوسطة، تكون الإعدادات الافتراضية في JVM كافية. لا تلجأ إلى الضبط اليدوي إلا عند وجود مؤشرات أداء واضحة تستدعي ذلك.
اضبط JVM Flags بوعي
تتيح لك وسائط JVM التحكم في:
- نوع جامع القمامة.
- الحجم الابتدائي والأقصى للذاكرة.
- حجم الأجيال المختلفة داخل
Heap. - سلوك الترقية بين الأجيال.
اربط الاختيار بطبيعة التطبيق
تطبيقات الويب التفاعلية تحتاج غالباً إلى تقليل التوقفات، بينما قد تتحمل تطبيقات batch processing فترات توقف أطول مقابل تحقيق إنتاجية أعلى.
الخلاصة التقنية
جمع القمامة في Java ليس مجرد ميزة مريحة، بل هو جزء أساسي من فلسفة اللغة في إدارة الذاكرة بأمان وكفاءة. فهم طريقة عمل GC داخل JVM يساعد المطور على تفسير سلوك التطبيق تحت الضغط، وتحليل مشاكل الأداء، واختيار الجامع الأنسب وفقاً للأولويات الفعلية مثل زمن الاستجابة، حجم الذاكرة، أو الإنتاجية. تقنياً، لا يحتاج كل مشروع إلى ضبط متقدم، لكن الإلمام بهذه المفاهيم يمنحك قدرة أكبر على بناء تطبيقات أكثر استقراراً وقابلية للتوسع.