بنية JVM والأداء

خوارزميات جمع المهملات والضبط

15 دقيقة الدرس 4 من 13

خوارزميات جمع المهملات والضبط

إن معرفة أيّ جامع مهملات يعمل ولماذا يتصرف بهذه الطريقة هو الفرق بين التخمين في أعلام JVM واتخاذ قرارات مدروسة وقابلة للقياس. يركّز هذا الدرس على جامعَي المهملات اللذَين ستقابلهما في الخدمات الإنتاجية الحديثة — G1GC وZGC — وأعلام تحديد حجم الذاكرة الأساسية التي تتحكّم في سلوكهما.

لماذا توجد خوارزميات مختلفة لجمع المهملات

كلّ جامع مهملات ينطوي على مقايضات هندسية عبر ثلاثة محاور:

  • الإنتاجية (Throughput) — إجمالي العمل الذي يؤدّيه التطبيق في وحدة زمنية (توقّفات GC تُقلّص هذه القيمة).
  • زمن الاستجابة (Latency) — أطول توقّف يمكن أن يواجهه طلب مستخدم واحد.
  • البصمة الذاكرية (Footprint) — قدر الذاكرة الذي يستهلكه الجامع نفسه لهياكله البيانية.

لا يتفوّق أيّ جامع على الآخرين في المحاور الثلاثة معًا. يبدأ اختيار الجامع المناسب من معرفة متطلّبات الخدمة: هل هدف زمن الاستجابة عند الشريحة المئوية التاسعة والتسعين أهمّ من الإنتاجية الإجمالية؟

الجامع ذو الأولوية للمهملات (G1GC)

G1 هو الجامع الافتراضي منذ JDK 9. فكرته الجوهرية تقسيم الذاكرة الكاملة إلى عدد كبير من المناطق المتساوية (عادةً بين 1 ميغابايت و32 ميغابايت، يختارها JVM تلقائيًا). المناطق لا تُخصَّص للجيلين الشاب أو القديم بصفة دائمة؛ بل يُعيد الجامع تصنيفها ديناميكيًا.

كيف يعمل G1 على مستوى عالٍ

  1. مرحلة الجيل الشاب فقط — يُجري G1 وضع علامات متزامنًا (Concurrent Marking) أثناء عمل التطبيق. توقّفات GC الصغيرة (STW) القصيرة تُخلّي المناطق الشابة من الكائنات الحية وتنقلها إلى مناطق الناجين أو القديم.
  2. مرحلة التجميع المختلط — بعد أن يُحدّد وضع العلامات المناطق القديمة التي تحتوي على أكبر قدر من المهملات، يضمّ G1 مجموعة من تلك المناطق في عمليات التجميع اللاحقة ("mixed GC"). ويجمع المناطق الأكثر مهملًا أولًا — ومن هنا اسم Garbage-First.
  3. Full GC — آخر خيار يلجأ إليه الجامع، وهو تجميع كامل مضغوط. قبل JDK 10 كان أحادي الخيط، أما بعده فهو متوازٍ. يجب تجنّبه في الإنتاج.
هدف توقّف G1: يسعى G1 للحفاظ على التوقّفات دون قيمة يحدّدها المستخدم (-XX:MaxGCPauseMillis، الافتراضي 200 ميلي ثانية) عبر تعديل عدد المناطق التي يجمعها في كلّ دورة. هذا الهدف تلميح لا ضمان.

أعلام G1 الأساسية

# تفعيل G1 صراحةً (افتراضي في JDK 9+، لكن التصريح به يوضّح القصد) -XX:+UseG1GC # هدف وقت التوقّف بالميلي ثانية (الافتراضي: 200) -XX:MaxGCPauseMillis=100 # عدد خيوط GC للعمل المتوازي (افتراضيًا يُحدَّد تلقائيًا حسب عدد المعالجات) -XX:ParallelGCThreads=8 # خيوط GC المتزامنة (وضع العلامات، التنقية — تعمل جنبًا إلى جنب مع التطبيق) -XX:ConcGCThreads=2 # بدء وضع العلامات المتزامن عندما تمتلئ الذاكرة بنسبة X% (الافتراضي: 45) # خفّضها إذا رأيت Full GCs متكررة — يمنح G1 وقتًا أطول لإنهاء وضع العلامات -XX:InitiatingHeapOccupancyPercent=35 # حجم المنطقة (قوة للعدد 2، من 1م إلى 32م)؛ عادةً اتركه للـ JVM -XX:G1HeapRegionSize=4m

سطر أوامر إنتاجي حقيقي قد يبدو كالتالي:

java -Xms4g -Xmx4g \ -XX:+UseG1GC \ -XX:MaxGCPauseMillis=150 \ -XX:InitiatingHeapOccupancyPercent=40 \ -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=5,filesize=20m \ -jar myapp.jar

ZGC — جمع المهملات بزمن استجابة فائق الانخفاض

ZGC (جاهز للإنتاج منذ JDK 15، أُضيف ZGC الجيلي في JDK 21) مصمَّم لهدف رئيسي واحد: توقّفات STW دون ميلي ثانية بصرف النظر عن حجم الذاكرة. يُحقّق هذا عبر تقنيتَين متقدّمتَين:

  • المؤشرات الملوّنة (Colored Pointers) — يُشفّر ZGC بيانات GC الوصفية (بتات العلامة، حالة الانتقال) مباشرةً داخل المرجع ذي 64 بت. هذا يتيح له القيام بعمل حاجز التحميل (load barrier) عند استخدام المرجع بدل مسح الذاكرة بأكملها أثناء التوقّف.
  • الانتقال المتزامن — خلافًا لـ G1 الذي ينقل الكائنات أثناء التوقّف، ينقل ZGC الكائنات الحية أثناء عمل التطبيق مستعملًا حاجز تحميل ذاتي الشفاء يُعيد توجيه المراجع القديمة بشفافية.
ما معنى "ZGC الجيلي" (JDK 21+): كان ZGC الأصلي غير جيلي — يعامل جميع الكائنات بالتساوي. يُضيف ZGC الجيلي تقسيمًا إلى جيل شاب وقديم، ويُحسّن الإنتاجية بشكل جذري باستغلال الفرضية الجيلية الضعيفة (معظم الكائنات تموت شابّة). فعّله بـ -XX:+ZGenerational في JDK 21.

أعلام ZGC الأساسية

# تفعيل ZGC -XX:+UseZGC # في JDK 21+ فعّل الوضع الجيلي (موصى به بشدة) -XX:+ZGenerational # ZGC يحترم MaxGCPauseMillis أيضًا لكن توقّفاته الفعلية أقلّ من 1 ميلي ثانية -XX:MaxGCPauseMillis=10 # خيوط GC المتزامنة (ZGC يُنجز كل شيء تقريبًا بشكل متزامن) -XX:ConcGCThreads=4

G1 مقابل ZGC: متى تختار أيّهما

المعيارG1GCZGC
هدف التوقّفعشرات إلى مئات الميلي ثانيةدون ميلي ثانية
عبء الإنتاجيةمنخفض (~5–10% مقارنةً بـ Parallel GC)أعلى قليلًا (حواجز التحميل)
الحجم المثالي للذاكرة4 جيجابايت – 32 جيجابايتأيّ حجم، حتى تيرابايتات
البصمة الذاكريةأدنىأعلى قليلًا (بيانات المؤشرات الملوّنة)
أدنى إصدار JDKJDK 9JDK 15 للإنتاج؛ JDK 21 للوضع الجيلي
حالة الاستخدام النموذجيةخدمات الويب، المعالجة الدُّفعية، الخدمات المصغّرةتداول فوري، ألعاب، واجهات برمجية حسّاسة للزمن
ابدأ بـ G1. إعداداته الافتراضية محسَّنة جيدًا لمعظم الخدمات. انتقل إلى ZGC فقط عندما تُثبت أدلّة التحليل أن توقّفات GC تؤثّر فعلًا على هدف زمن الاستجابة — ليس بناءً على التخمين. الضبط المبكّر يُضيّع وقت الهندسة ويُضيف مخاطر.

أعلام تحديد حجم الذاكرة: أساس ضبط GC

قبل لمس أيّ علم خاص بالجامع، اضبط حجم الذاكرة بشكل صحيح. هذه الأعلام الثلاثة تنطبق على كلّ جامع:

# الحجم الأدنى للذاكرة — يبدأ JVM بهذا الحجم على الأقل -Xms2g # الحجم الأقصى للذاكرة — لا يتجاوز JVM هذا الحدّ -Xmx2g # موصى به: اضبط Xms == Xmx في الإنتاج # يمنع JVM من طلب ذاكرة من نظام التشغيل أو إعادتها، # وهو ما يتجنّب توقّفات غير متوقّعة على مستوى نظام التشغيل ويُسرّع الإقلاع. # الحجم الأقصى للـ Metaspace (بيانات الفئات الوصفية، لا تُحسب ضمن Xmx) -XX:MaxMetaspaceSize=256m
لا تضبط -Xmx أعلى من ذاكرة RAM الفعلية المتاحة ناقصًا احتياجات نظام التشغيل والعمليات الأخرى. إذا اضطرّ JVM إلى استخدام الـ Swap، تصبح كلّ دورة GC بطيئة كارثيًا. قاعدة شائعة: اترك 1–2 جيجابايت على الأقل لنظام التشغيل والعبء الإضافي لـ JVM (خيوط، مخازن JIT، Metaspace، المخازن المباشرة) عند ضبط -Xmx.

تفعيل سجلّ GC (ضروري للضبط)

لا يمكنك ضبط ما لا تقيسه. فعّل دائمًا سجلّ GC في الإنتاج:

# صياغة التسجيل الموحّد في JDK 9+ -Xlog:gc*:file=/var/log/app/gc.log:time,uptime,level,tags:filecount=10,filesize=20m

يكتب هذا سجلّات GC دوّارة مع طوابع زمنية. أرسلها إلى GCEasy (gceasy.io) أو GCViewer للتحليل البصري — ستجد فورًا توزيعات وقت التوقّف ومعدّلات التخصيص ومعرفة ما إذا كانت Full GC تحدث.

تهيئة عملية كنقطة بداية

// مثال: خدمة Spring Boot، حاوية بـ 8 جيجابايت، حسّاسة لزمن الاستجابة // تشغيل مع الأعلام التالية: // java [الأعلام التالية] -jar service.jar -Xms6g -Xmx6g -XX:MaxMetaspaceSize=256m -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:InitiatingHeapOccupancyPercent=40 -Xlog:gc*:file=/var/log/service/gc.log:time,uptime,level,tags:filecount=10,filesize=20m

قِس أوقات التوقّف من سجلّ GC بعد التشغيل تحت حمل واقعي قبل تغيير أيّ شيء آخر. اضبط علمًا واحدًا في المرة وأعِد القياس — ضبط GC تجريبيٌّ بطبيعته، وليس قائمة مراجعة.

الخلاصة

  • G1GC: يعتمد المناطق وهدف وقت التوقّف، افتراضي منذ JDK 9، ممتاز للذاكرة من 4 إلى 32 جيجابايت.
  • ZGC: انتقال متزامن بمؤشرات ملوّنة، توقّفات دون ميلي ثانية، الوضع الجيلي في JDK 21 يُحسّن الإنتاجية بشكل جذري.
  • تحديد حجم الذاكرة: اضبط -Xms مساويًا لـ -Xmx في الإنتاج، اترك هامشًا لنظام التشغيل، وحدّد سقفًا للـ Metaspace.
  • سجّل دائمًا مخرجات GC وحلّلها قبل ضبط الأعلام — قِس أولًا، ثم غيّر.