جامع القمامة: ليه تطبيقك بيتجمّد فجأة، وإزاي تقيس التوقّف وتقلّله
المستوى المطلوب: محترف — الشرح يفترض إنك بتكتب بلغة فيها جامع قمامة (Go أو Java أو C#) وبتتعامل مع قياس أداء الخوادم و p99 latency.
لو خدمتك بترد في 20 مللي ثانية غالبًا، وفجأة طلب كل فترة بياخد 200 مللي ثانية من غير أي سبب في الكود بتاعك، المتّهم الأول هو جامع القمامة. المقال ده هيوريك ليه بيحصل التوقّف ده بالظبط، إزاي تقيسه بنفسك برقم، وإزاي تقلّله من غير ما تكسر حاجة.
المشكلة باختصار
أي لغة بجامع قمامة بتخصّص ذاكرة للكائنات وقت التشغيل. الكائنات اللي مبقاش ليها لازمة لازم ترجع للنظام، وإلا الذاكرة تمتلي والبرنامج يقع بـ OOM. جامع القمامة بيعمل التنظيف ده لوحده. الثمن المخفي: عشان ينضّف بأمان، بيحتاج في لحظة معيّنة يوقف كل خيوط تطبيقك. اللحظة دي اسمها Stop-The-World، وهي مصدر التجمّد المفاجئ اللي بيظهر في ذيل الـ latency مش في المتوسط.
مثال قبل النظرية: عامل النظافة اللي بيوقف الحفلة
تخيّل حفلة في شقة. عامل النظافة عايز يجمع الأكواب الفاضية بس. لو لفّ وسط الناس وهم بيتحركوا، ممكن يرمي كوب لسه حد ماسكه. فبيعمل حاجة قاسية: يقول للجميع «اثبتوا مكانكم»، يلفّ بسرعة، يشوف كل كوب متمسوك بإيد ولا لأ، يرمي اللي مش متمسوك، وبعدين يقول «اتحرّكوا». اللحظة اللي الكل بيثبت فيها دي بالظبط هي الـ Stop-The-World.
دلوقتي بالتعريف العلمي: الكائن يعتبر «حي» لو جامع القمامة يقدر يوصله انطلاقًا من الجذور (الـ roots): المتغيّرات على الـ stack، والـ globals، ومحتوى الـ registers. الخوارزمية الكلاسيكية اسمها Mark and Sweep: في مرحلة الـ Mark بيتتبّع كل مؤشر من الجذور ويعلّم كل كائن يوصله؛ وفي مرحلة الـ Sweep بيحرّر أي كائن مامتعلّمش. الكائنات اللي مفيش أي مسار يوصلها من الجذور هي القمامة اللي بتترمي.
ليه في توقّف أصلاً
لو الكود بتاعك بيعدّل المؤشرات في نفس اللحظة اللي الـ GC بيتتبّع فيها، ممكن يفوته كائن حي (فيترمي حاجة لسه مستخدمة، وده كارثة)، أو يعدّ كائن ميت على إنه حي (تسريب مؤقت). الحل التاريخي البسيط: أوقف كل الخيوط طول مرحلة التتبّع. ده آمن لكنه بيوقف الخدمة.
الجامعات الحديثة بتقلّل التوقّف بشكل كبير بإنها تعمل معظم الـ marking بالتوازي مع تشغيل التطبيق (tracing متزامن مع tri-color marking و write barriers)، وبتوقف العالم في شريحة صغيرة جدًا بس. النتيجة العملية بالأرقام:
- Go: توقّفات الـ STW عادة أقل من مللي ثانية، وكتير أقل من 100 ميكروثانية، بغضّ النظر عن حجم الـ heap تقريبًا.
- Java G1: الهدف الافتراضي
-XX:MaxGCPauseMillis=200، لكن الـ Full GC على heap كبير ممكن يوصل لثواني فعلية. - ZGC / Shenandoah: توقّفات تحت المللي ثانية حتى على heaps بحجم مئات الجيجابايت.
يعني «التجمّد 200 مللي ثانية» واقعي جدًا على JVM بإعداداته الافتراضية أو تحت ضغط تخصيص عالي، وأقل شيوعًا على Go الحديث. بس المبدأ واحد: التوقّف موجود، والسؤال هو قد إيه وإمتى.