السحر الذي يجب أن تفهمه
\n\nكل من كتب بلغة C# لمدة أسبوعين على الأقل يعلم: بيئة التشغيل (CLR) هي التي تدير الذاكرة. أنت فقط تنشئ الكائنات باستخدام new، وعندما تصبح غير ضرورية — يأتي جامع القمامة (GC) وينظف كل شيء. يبدو وكأنه سحر، أليس كذلك؟ لكن وراء هذا السحر تكمن هندسة صارمة، وإذا لم تفهمها — سيبدأ تطبيقك في "البطء"، أو "استهلاك" غيغابايتات من RAM، أو حتى الانهيار مع OutOfMemoryException.
في هذه المقالة، سنلقي نظرة تحت غطاء CLR ونتعرف على كيفية عمل إدارة الذاكرة في C# بالضبط. ستتعرف على الفرق بين المكدس والكومة، وما هي أجيال الكائنات، ولماذا IDisposable هو أفضل صديق لك، وكيف لا تخطو على أشواك Large Object Heap. هيا بنا!
المكدس والكومة: الأساس الذي يتم التكتم عليه في الكتب الدراسية
\n\nعندما يعمل برنامجك، يقسم CLR الذاكرة إلى منطقتين رئيسيتين: المكدس (Stack) والكومة (Heap). فهم الفرق بينهما هو الأساس الذي بدونه يفقد كل شيء آخر معناه.
\n\nالمكدس — سريع ولكنه صغير
\nالمكدس هو منطقة ذاكرة تُخزن فيها أنواع القيم (value types) (int, bool, struct, enum) ومراجع الكائنات. يعمل وفق مبدأ LIFO (Last In, First Out). عندما تستدعي دالة، يتم تخصيص إطار (frame) لها في المكدس. بمجرد انتهاء الدالة — يتم تدمير الإطار فورًا. لا حاجة لجامع القمامة هنا. سرعة الوصول إلى المكدس هي الأقصى، لكن الحجم محدود (عادةً 1-4 ميجابايت لكل خيط).
\n\nvoid Example()\n{\n int x = 42; // x موجود في المكدس\n string s = "مرحباً"; // المرجع s في المكدس، الكائن نفسه — في الكومة\n}\n\n\nالكومة — أبطأ ولكنها أوسع
\nالكومة (Managed Heap) — هي الذاكرة "طويلة الأمد". هنا تعيش جميع أنواع المراجع (reference types) (الفئات، السلاسل النصية، المصفوفات، المفوضين). يحدث تخصيص الذاكرة في الكومة مع كل new. وهنا يأتي دور جامع القمامة. هو الذي يقرر متى يمكن تحرير الذاكرة، ومتى يجب الانتظار.
هام: المكدس ليس "سيئًا"، والكومة ليست "جيدة". لكل منهما مهمته الخاصة. أنواع القيم سريعة، لكن لا يمكن تمريرها بالمرجع بدون الكلمة المفتاحية ref. أنواع المراجع مرنة، لكنها تتطلب GC.
كيف يعمل جامع القمامة في C#: الأجيال والخوارزمية
\n\nجامع القمامة في .NET ليس مجرد "حذف ونسيان". إنه آلية ذكية تستخدم الأجيال (Generations) لتقليل فترات التوقف في عمل التطبيق.
\n\nثلاثة أجيال من الكائنات
\n- \n
- Gen 0 — الكائنات حديثة الولادة. تعيش لفترة قصيرة. جمع Gen 0 يحدث بشكل متكرر وسريع جدًا. \n
- Gen 1 — "الناجون" بعد جمع واحد. منطقة عازلة بين الكائنات الشابة والقديمة. \n
- Gen 2 — المعمرون. جمع Gen 2 هو الأغلى والأندر. إذا نجا كائن حتى Gen 2، فمن المحتمل أن يبقى هناك لفترة طويلة. \n
خوارزمية عمل GC بشكل مبسط تبدو كالتالي:
\n- \n
- يطلب التطبيق ذاكرة. إذا لم تكن هناك مساحة في الكومة — يبدأ الجمع. \n
- يبني GC رسمًا بيانيًا للكائنات القابلة للوصول، بدءًا من الجذور (الحقول الثابتة العامة، المتغيرات المحلية في مكدسات الخيوط، سجلات CPU). \n
- يتم وضع علامة على جميع الكائنات غير القابلة للوصول على أنها "قمامة". \n
- يحدث الضغط (compaction) — يتم نقل الكائنات الحية لتجنب التجزئة. \n
- تُعاد الذاكرة المحررة إلى المجموعة. \n
إليك مثال توضيحي لكيفية تأثير GC على الأداء:
\n\nclass HeavyObject\n{\n public byte[] Data = new byte[1024 * 1024]; // 1 ميجابايت\n}\n\nvoid TestGC()\n{\n for (int i = 0; i < 1000; i++)\n {\n var obj = new HeavyObject(); // في كل مرة نخصص 1 ميجابايت في الكومة\n // obj يخرج عن النطاق — يصبح قمامة\n }\n // بعد الحلقة، قد يقوم GC بجمع Gen 0 ليس