المستوى: متوسط — الكلام ده مبني على فرضية إنك بتكتب بايثون (CPython تحديداً) وعندك إلمام بالكلاسات والمتغيرات.
لو عندك خدمة بايثون شغّالة على مدار اليوم وبتاكل ذاكرة كل ساعة لحد ما توقع بـ OOM، المشكلة في الغالب مش في الكود اللي بتشوفه قدامك. المشكلة في كائنات بتشاور على بعض ومحدش بيحررها. هنا هتعرف بالظبط ليه ده بيحصل، تقيس التسريب بنفسك بالأرقام، وتصلحه في سطر واحد.
جامع المهملات في بايثون: ليه ذاكرتك بتتسرّب رغم الـ GC
المشكلة باختصار
في لغات زي C لازم تنادي free() بإيدك لكل حاجة حجزتها. بايثون بيوفرلك ده: بيحرر الذاكرة لوحده. بس "لوحده" دي ليها تفاصيل، ولما متفهمهاش بتدفع التمن ذاكرة بتتسرّب في الإنتاج بدون أي رسالة خطأ.
الآلية الأساسية: العدّ المرجعي (Reference Counting)
تخيّل مكتبة فيها كتاب، وجنب كل كتاب ورقة مكتوب فيها كام شخص ماسك الكتاب دلوقتي. كل ما حد ياخد نسخة الرقم بيزيد واحد، وكل ما حد يرجّعها بيقل واحد. أول ما يوصل صفر، أمين المكتبة بيرمي الكتاب فوراً. ده بالظبط اللي بايثون بيعمله مع كل كائن.
علمياً: كل كائن في CPython جواه عدّاد اسمه reference count. كل ما متغيّر يشاور عليه يزيد، وكل ما تعمل del أو المتغير يخرج من النطاق يقل. أول ما يوصل صفر، الذاكرة بتترجع في نفس اللحظة. شوف بنفسك:
import sys
x = []
print(sys.getrefcount(x)) # 2 (واحد لـ x، وواحد مؤقت لوسيط الدالة)
y = x
print(sys.getrefcount(x)) # 3
del y
print(sys.getrefcount(x)) # 2
الرقم بيطلع 2 مش 1 لأن استدعاء getrefcount نفسه بيعمل مرجع مؤقت. ركز على الفرق بين السطور. الخلاصة: العدّ المرجعي حتمي وفوري، وده ميزة كبيرة. لكن عنده ثغرة واحدة قاتلة.
المشكلة الحقيقية: الدورة المرجعية (Reference Cycle)
الطريقة دي بتفشل في حالة واحدة: لما كائنين يشاوروا على بعض. تخيّل صديقين كل واحد ماسك إيد التاني ومصمم ميسيبش غير لما التاني يسيب الأول. النتيجة؟ محدش بيسيب أبداً. العدّاد بتاع كل واحد بيفضل 1 للأبد، حتى لو مفيش حد تاني في البرنامج بيعرفهم.
ده بيحصل بشكل طبيعي: عقدة في شجرة بتمسك أبوها وأبوها بيمسكها، أو كائن فيه callback بيشاور على نفسه. العدّ المرجعي لوحده عمره ما هيحرر الدورة دي. عشان كده عند بايثون طبقة تانية: جامع الدورات (cyclic garbage collector) في موديول gc، اللي بيمشي من الجذور وبيرمي أي حاجة مش قادر يوصلها. خلينا نقيس: