المستوى المطلوب: متوسط — هذا المقال يفترض إنك تعرف JavaScript الأساسي و Node.js و فكرة الـ heap memory بشكل عام، ومش بتكتب لأول مرة في حياتك.
لو Node.js بتاعك بيستهلك 280MB بعد ساعة، وبعد 8 ساعات وصل 3.8GB، والـ traffic ما زادش ولا حتى 10%، إنت مش محتاج سيرفر أكبر. عندك memory leak. الـ Garbage Collector في V8 شغّال صح، بس الكود بتاعك بيمسك references مش لازمة فبيمنعه يحرر الذاكرة. المقال ده بيوريك ليه ده بيحصل، ازاي تكتشفه في 5 دقايق، وازاي تحلّه بـ 6 سطور كود.
Garbage Collection في JavaScript: ليه السيرفر بياكل ذاكرة بدون توقّف
المشكلة باختصار
JavaScript مش بتحمّلك إدارة الذاكرة يدويًا زي C أو Rust. الـ Garbage Collector (اختصارًا GC) بيعمل ده تلقائيًا. الفرضية الأساسية بسيطة: أي object مفيش حد بيشاور عليه = ممكن يتمسح. المشكلة بتبدأ لمّا الكود بتاعك يفضل ماسك مرجع للكائن بدون قصد — الـ GC بيشوفه "حي" وبيسيبه. النتيجة: الذاكرة بتزيد كل دقيقة لحد ما الـ process يقع بـ Out Of Memory الساعة 3 الصبح.
مثال بسيط للمبتدئين: مكتب الموظفين وعامل النظافة
تخيّل معاك شركة فيها 50 موظف، وكل موظف عنده درج أوراق على مكتبه. فيه عامل نظافة بيمر كل ساعة، شغلته إنه يجمع الأوراق اللي محدش طلبها. الموظف اللي رمى ورقة في سلة المهملات، النظافة بياخدها فورًا. بس لو الموظف نسي ورقة على المكتب من غير ما يحتاجها، النظافة هيسيبها — لأن "ممكن حد يحتاجها".
الـ GC في JavaScript بنفس الذكاء ونفس القيد. كل كائن في الـ memory ليه "متابعين": متغيرات، arrays، objects تانية بتشاور عليه. لما المتابعين كلهم يختفوا، الكائن يبقى مرشّح للحذف في الدورة الجاية. بس متغيّر واحد ناسي بيمسك الكائن = الكائن مكانه مكانه طول عمر التطبيق.
التعريف العلمي: ازاي V8 بيشتغل بالظبط
محرّك V8 (نفسه في Chrome و Node.js) بيستخدم خوارزمية Generational Mark-and-Sweep. الـ heap مقسوم لقسمين رئيسيين:
- Young Generation (الـ Scavenger): الكائنات الجديدة بتتحط هنا، وأغلبها بيموت بسرعة (الفرضية اسمها Generational Hypothesis). الـ GC هنا سريع جدًا (~1ms) وبيتم بـ Cheney's Algorithm.
- Old Generation (Mark-Compact): الكائنات اللي عاشت أكتر من دورتين Scavenge بتنتقل هنا. الـ GC هنا أبطأ (10–100ms) لكن بيتم بشكل تدريجي (incremental marking) عشان مايوقفش الـ event loop.
الـ root set هو نقاط البداية اللي الـ GC بيبدأ منها البحث: الـ global scope، stack المحلي، closures نشطة. أي كائن يقدر الـ GC يوصله من الـ root بسلسلة references، يبقى "reachable" — يعني حي. أي كائن مش reachable، يتمسح. الفكرة دي اسمها reachability analysis.