هذا المقال يتطلّب مستوى: متوسط.
لو خدمتك بتتجمّد لجزء من الثانية كل شوية والـ CPU تقريبًا فاضي، السبب غالبًا مش كودك — ده جامع المهملات بيشتغل. في نهاية المقال هتعرف بالظبط إمتى بيشتغل، ليه بيوقف برنامجك، وإزاي تقلّل الوجع ده برقم.
الـ Garbage Collection: إزاي البرنامج بيفضّي الذاكرة لوحده
المشكلة باختصار
كل كائن بتعمله في برنامجك بياخد مساحة في الذاكرة. لو محدش فضّاها بعد ما تخلص منها، الذاكرة بتزيد لحد ما البرنامج يقع بـ Out Of Memory. في لغات زي C لازم تفضّيها بإيدك، وده مصدر أخطر الأخطاء. في بايثون وجافا وجافاسكريبت وGo، فيه حارس شغّال في الخلفية اسمه جامع المهملات (Garbage Collector) بيفضّيها بدالك. المشكلة إن الحارس ده نفسه بياخد وقت، وساعات بيوقف برنامجك كله عشان يكنس.
المفهوم بمثال أولًا
تخيّل فندق فيه غرف. كل نزيل يحجز غرفة (كائن في الذاكرة). لما النزيل يمشي، الغرفة تفضل مقفولة ومحجوزة على ورق حتى لو فاضية. لو محدش بيشيل الحجوزات القديمة، هتوصل لحظة تقول فيها "مفيش غرف" رغم إن نصهم فاضي.
جامع المهملات هو عامل الاستقبال اللي بيلف كل فترة، يبص مين لسه ماسك مفتاح غرفة فعلًا، وأي غرفة محدش ماسك مفتاحها يعتبرها فاضية ويرجّعها للمتاح. عشان يعدّ بدقّة، لازم يوقف حركة الفندق لحظة — وده بالظبط الـ "stop-the-world" اللي بتحسّه كتجمّد بسيط.
نفس الكلام علميًا
فيه طريقتين أساسيتين، وأغلب اللغات بتخلط بينهم:
- عدّ المراجع (Reference Counting): كل كائن معاه عدّاد بيزيد لما حاجة تشاور عليه، وينقص لما تسيبه. أول ما العدّاد يوصل صفر، الكائن يتفضّى فورًا. ده أساس بايثون (CPython).
- التتبّع (Tracing / Mark-and-Sweep): الجامع يبدأ من الجذور (المتغيّرات الحيّة)، يمشي على كل كائن يقدر يوصله ويعلّمه "حيّ"، وأي كائن مش متعلّم يكنسه. ده أساس جافا وGo وجافاسكريبت.
عدّ المراجع بسيط وفوري، لكن عنده ثغرة واحدة قاتلة: المرجع الدائري. كائنان يشاور كل واحد على التاني، فالعدّاد لأي منهم عمره ما يوصل صفر، حتى لو مفيش حد تاني ماسكهم. عشان كده بايثون بيضيف فوق عدّ المراجع جامعًا متتبّعًا يمسك الحلقات دي.
مثال تنفيذي: شوف التسريب بنفسك
الكود ده بيصنع مرجعًا دائريًا، بيوقف الجامع المتتبّع، وبيوريك إن الذاكرة مش بترجع إلا لما تشغّله يدويًا:
import gc
class Node:
def __init__(self):
self.ref = None
gc.disable() # نوقف الجامع المتتبّع عشان نشوف التسريب بوضوح
def make_cycle():
a = Node()
b = Node()
a.ref = b # a بيشاور على b
b.ref = a # b بيشاور على a -> حلقة
# a و b هيخرجوا من النطاق هنا، لكن عدّاد المراجع بتاعهم = 1 لكل واحد
for _ in range(100_000):
make_cycle()
print("كائنات لسه عايشة قبل الكنس:", len(gc.get_objects()))
freed = gc.collect() # نشغّل الجامع المتتبّع يدويًا
print("كائنات كنسها الجامع:", freed)