أحمد حايس
الرئيسيةمن أناالدوراتالمدونةسوق الأوامرالمناهج والباقاتالشركاء
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • سوق الأوامر
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالمناهجالمدونةالدخول
البرمجة بالعربي

جامع القمامة: ليه تطبيقك بيتجمّد فجأة، وإزاي تقيس التوقّف وتقلّله

محترف13 يوليو 20265 دقائق قراءة
جامع القمامة: ليه تطبيقك بيتجمّد فجأة، وإزاي تقيس التوقّف وتقلّله

جامع القمامة: ليه تطبيقك بيتجمّد فجأة، وإزاي تقيس التوقّف وتقلّله

المستوى المطلوب: محترف — الشرح يفترض إنك بتكتب بلغة فيها جامع قمامة (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 الحديث. بس المبدأ واحد: التوقّف موجود، والسؤال هو قد إيه وإمتى.

قِس توقّفك بنفسك (كود Go شغّال)

بدل التخمين، قِس. الكود ده بيولّد قمامة بشكل متعمّد وبعدين بيطبع إحصائيات الـ GC من runtime.MemStats:

Go
package main

import (
	"fmt"
	"runtime"
	"runtime/debug"
	"time"
)

func main() {
	debug.SetGCPercent(100)

	start := time.Now()
	sink := make([][]byte, 0, 512)
	for i := 0; i < 200000; i++ {
		b := make([]byte, 1024)
		sink = append(sink, b)
		if len(sink) > 512 {
			sink = sink[256:]
		}
	}

	var m runtime.MemStats
	runtime.ReadMemStats(&m)

	n := m.NumGC
	if n == 0 {
		n = 1
	}
	fmt.Printf("GC cycles: %d\n", m.NumGC)
	fmt.Printf("total pause ms: %.3f\n", float64(m.PauseTotalNs)/1e6)
	fmt.Printf("avg pause ms: %.3f\n", float64(m.PauseTotalNs)/float64(n)/1e6)
	fmt.Printf("wall: %s\n", time.Since(start))
}

شغّله عادي بـ go run main.go. عشان تشوف كل دورة GC لحظيًا شغّله بـ GODEBUG=gctrace=1 go run main.go. على جهاز عادي هتلاقي متوسط التوقّف الواحد أقل من مللي ثانية بكتير — وده بالظبط اللي المفروض توثّقه من خدمتك الحقيقية قبل ما تلمس أي إعداد.

اللفّة الأساسية للتقليل: GOGC

أهم مقبض في Go اسمه GOGC (أو debug.SetGCPercent). قيمته الافتراضية 100 معناها: شغّل دورة GC جديدة لما الـ heap يكبر بنسبة 100% فوق حجم الكائنات الحية بعد آخر تنظيف. لو رفعته لـ 400، الـ heap بيكبر أربع أضعاف قبل كل تنظيف، فعدد الدورات بيقلّ لحوالي الرُبع واستهلاك CPU في الـ GC بيقلّ معاه — بس الذاكرة بتزيد.

الافتراض هنا: خدمتك allocation-heavy وبتشتغل على Go 1.19+. في الحالة دي، رفع GOGC بيقلّل ضغط الـ GC assist (لحظات بيتباطأ فيها التطبيق عشان يساعد الجامع)، فينعكس تحسّن على p99. المقابل إنك بتقايض ذاكرة أكتر مقابل تنظيف أقل.

الـ trade-offs بوضوح

  • ترفع GOGC (مثلاً 100 ← 400): بتكسب دورات GC أقل بحوالي 4 أضعاف واستهلاك CPU أقل و p99 أهدأ. بتخسر ذاكرة — الـ peak heap ممكن يتضاعف تقريبًا (رقم تقديري بيعتمد على حجم الكائنات الحية عندك).
  • تنزّل GOGC (مثلاً 100 ← 50): بتكسب ذاكرة أقل. بتخسر CPU أكتر ودورات GC أكتر، وده ممكن يزوّد الـ latency مش يقلّلها.
  • حاجز أمان: استخدم GOMEMLIMIT (سقف ذاكرة ليّن من Go 1.19) بدل ما ترفع GOGC على البحري. كده بتخلي الجامع «كسول» طول ما في ذاكرة، ويشدّ حيله أوتوماتيك قبل ما تقرّب من السقف.

متى متشغلش بالك بالـ GC

ضبط الـ GC قبل ما تقيس مشكلة فعلية هو من أشهر أنواع تضييع الوقت في الباك-إند. متلمسش GOGC لو:

  • خدمتك batch job أو CLI عمرها ثوانٍ — التوقّفات مش هتفرق أصلاً.
  • الـ p99 latency عندك مريح والذاكرة مش مضغوطة — مفيش مكسب تكسبه.
  • مقستش. من غير رقم قبل وبعد، انت بتخمّن مش بتحسّن.

الخطوة التالية

شغّل خدمتك الحقيقية بـ GODEBUG=gctrace=1 لمدة دقيقة تحت حمل واقعي، وسجّل حاجتين: أطول توقّف STW، وعدد دورات GC في الدقيقة. لو أطول توقّف تحت مللي ثانية والذاكرة مريحة، سيبها وخلّص. لو الـ GC بياكل نسبة CPU محسوسة وعندك RAM فاضية، ارفع GOGC لـ 200، قِس تاني، وقارن الرقمين. القرار لازم يطلع من الأرقام مش من الإحساس.

المصادر

  • A Guide to the Go Garbage Collector — go.dev (شرح GOGC والـ soft memory limit).
  • runtime.MemStats — pkg.go.dev (حقول NumGC و PauseTotalNs).
  • runtime/debug.SetGCPercent — pkg.go.dev.
  • Oracle: G1 Garbage Collector — MaxGCPauseMillis.
  • Oracle: The Z Garbage Collector (ZGC).

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة